Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1492dcf71f | ||
|
|
cd498c8c3a | ||
|
|
0961b23b8d | ||
|
|
3dffc82f1e | ||
|
|
68db5fe7b2 | ||
|
|
596d2f68c2 | ||
|
|
a1e5baacac | ||
|
|
cc0e4c19c8 | ||
|
|
5ab18c7b46 | ||
|
|
92d782e6c5 | ||
|
|
0fe5c75eae | ||
|
|
5357bc8915 | ||
|
|
a795307664 | ||
|
|
2ea22b392a | ||
|
|
239762fd3a | ||
|
|
62dd7ddb9c | ||
|
|
e2d9ddd516 | ||
|
|
0626e8c6cc | ||
|
|
bcbb297116 | ||
|
|
8fdbabf587 | ||
|
|
842046ac82 | ||
|
|
50d19b2c11 | ||
|
|
a380025a2b | ||
|
|
9fc26266cd | ||
|
|
35f502ad93 | ||
|
|
e51305b200 | ||
|
|
c8343a096e | ||
|
|
3bb16ff116 | ||
|
|
890510c4c6 | ||
|
|
c1406f8828 | ||
|
|
7f1840feb5 | ||
|
|
d89236d7a5 | ||
|
|
90253673ba | ||
|
|
e85f6e91e1 | ||
|
|
4bb138eb92 | ||
|
|
c0dc707a51 | ||
|
|
e19433a389 | ||
|
|
a048056b6b | ||
|
|
52e1ce0b20 | ||
|
|
41cbaf8be9 | ||
|
|
dacc439b1b | ||
|
|
caa335a319 | ||
|
|
3d397b38b4 | ||
|
|
9870697df9 | ||
|
|
c6c2fdb67b | ||
|
|
4d2e11e419 | ||
|
|
2be16d9244 | ||
|
|
aabc41aecc | ||
|
|
b0eff6a742 | ||
|
|
9f766d8296 | ||
|
|
9ba6e1b0e3 |
+5
-3
@@ -1,3 +1,7 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Build outputs
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -11,14 +15,12 @@ Thumbs.db
|
||||
|
||||
# Cache & Storage
|
||||
.cache/
|
||||
.sass-cache/
|
||||
admin/storage/cache/
|
||||
admin/storage/geoip/
|
||||
admin/storage/stats.json
|
||||
var/
|
||||
|
||||
# Runtime-compiled theme assets (generated by scssphp, read-only)
|
||||
themes/*/assets/css_compiled/
|
||||
|
||||
# Runtime-compiled theme assets
|
||||
public/themes/
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Disable directory listing globally
|
||||
Options -Indexes
|
||||
|
||||
# Security - Block access to entire application
|
||||
<Files ~ "^\.">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
<FilesMatch "\.(php|ini|log|conf|config|md|map)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to backup files
|
||||
<FilesMatch "\.(backup|bak|old|orig|swp|save)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to all application files
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
|
||||
# Directory protection - Block all access
|
||||
<Directory />
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Directory>
|
||||
|
||||
# Only allow access to public directory
|
||||
<Directory "public">
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Set default directory to public
|
||||
DirectoryIndex public/index.php
|
||||
|
||||
# Redirect root to public directory
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Redirect root to public
|
||||
RewriteRule ^$ public/ [L]
|
||||
|
||||
# Redirect all other requests to public
|
||||
RewriteCond %{REQUEST_URI} !^/public/
|
||||
RewriteRule ^(.*)$ public/$1 [L]
|
||||
</IfModule>
|
||||
@@ -0,0 +1,115 @@
|
||||
# Agent Instructions for CodePress CMS
|
||||
|
||||
## AI Model
|
||||
- **Huidig model**: `claude-opus-4-6` (OpenCode / `opencode/claude-opus-4-6`)
|
||||
- Sessie gestart: 16 feb 2026
|
||||
|
||||
## Build & Run
|
||||
- **Run Server**: `php -S localhost:8080 cms/router.php` (router nodig voor clean URLs)
|
||||
- **Lint PHP**: `find . -name "*.php" -not -path "./vendor/*" -exec php -l {} \;`
|
||||
- **Dependencies**: Composer vereist voor CommonMark, Twig en scssphp. Geen NPM.
|
||||
- **Admin Console**: Toegankelijk op `/admin.php` (standaard login: `admin` / `admin`)
|
||||
|
||||
## Project Structuur
|
||||
```
|
||||
codepress/
|
||||
├── cms/ # Core CMS engine
|
||||
│ ├── core/
|
||||
│ │ ├── class/
|
||||
│ │ │ ├── CodePressCMS.php # Hoofd CMS class
|
||||
│ │ │ ├── ThemeManager.php # Thema-resolver + Twig render + SCSS compile
|
||||
│ │ │ └── Logger.php # Logging systeem
|
||||
│ │ ├── plugin/
|
||||
│ │ │ ├── PluginManager.php # Plugin loader
|
||||
│ │ │ └── CMSAPI.php # API voor plugins
|
||||
│ │ ├── config.php # Config loader (leest config.json)
|
||||
│ │ └── index.php # Bootstrap (autoloader, requires)
|
||||
│ └── router.php # PHP dev server router (serveert ook /themes/)
|
||||
├── language/ # Taalbestanden (nl/, en/, de/ — elk met site.php + admin.php)
|
||||
├── themes/ # Dynamische thema's (volledig zelfstandig)
|
||||
│ ├── default/ # Standaard thema
|
||||
│ │ ├── theme.json # { title, default_layout, layout→.twig mapping, kleuren }
|
||||
│ │ ├── base.twig # Hoofd layout (head, header, nav, footer)
|
||||
│ │ ├── full_content.twig # Layout: volledige breedte
|
||||
│ │ ├── left_sidebar.twig # Layout: sidebar links
|
||||
│ │ ├── right_sidebar.twig # Layout: sidebar rechts
|
||||
│ │ ├── custom1.twig # Layout: custom
|
||||
│ │ ├── guide.twig # Layout: handleiding
|
||||
│ │ ├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
│ │ ├── assets/scss/theme.scss # SCSS bron (runtime gecompileerd)
|
||||
│ │ └── assets/js/ # app.js, bootstrap.bundle.min.js
|
||||
│ ├── demo/ # Demo thema (zelfde structuur, andere look)
|
||||
├── admin/ # Admin paneel
|
||||
│ ├── config/
|
||||
│ │ ├── app.php # Admin app configuratie
|
||||
│ │ ├── admin.json # Gebruikers & security (file-based, gitignored)
|
||||
│ │ └── admin.json.example # Voorbeeld met placeholder-wachtwoord
|
||||
│ ├── src/
|
||||
│ │ └── AdminAuth.php # Authenticatie (sessies, bcrypt, CSRF, lockout)
|
||||
│ ├── theme/default/views/ # Twig templates
|
||||
│ │ ├── login.twig # Login pagina
|
||||
│ │ ├── layouts/admin.twig # Admin layout met sidebar
|
||||
│ │ └── pages/ # dashboard, content, content-edit, config, plugins, theme, users, statistics, logs, security, update, guide, media, etc.
|
||||
│ └── storage/logs/ # Admin logs (gedeeld met front-end)
|
||||
├── 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/
|
||||
│ └── Navigation/
|
||||
├── public/ # Web root
|
||||
│ ├── assets/css/js/
|
||||
│ ├── index.php # Website entry point
|
||||
│ └── admin.php # Admin entry point + router
|
||||
├── content/ # Content bestanden
|
||||
├── guide/ # Handleidingen (nl/en)
|
||||
├── docs/ # Documentatie
|
||||
├── config.json # Site configuratie
|
||||
└── AGENTS.md # Dit bestand
|
||||
```
|
||||
|
||||
## Code Style & Conventions
|
||||
- **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 `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/src/AdminAuth.php`
|
||||
- **Content**: Stored in `content/`. Supports `.md` (Markdown), `.php` (Dynamic), `.html` (Static).
|
||||
- **Templating**: Twig templates in `themes/<naam>/`. `ThemeManager` rendert via Twig en compileert `assets/scss/theme.scss` runtime naar `public/themes/<naam>/theme.css`. Layout gekozen via frontmatter `layout:` key; onbekende layouts vallen terug op `default_layout` in `theme.json`.
|
||||
- **Navigation**: Auto-generated from directory structure. Folders require an index file to be clickable in breadcrumbs.
|
||||
- **Security**:
|
||||
- Always use `htmlspecialchars()` for outputting user/content data
|
||||
- Use `realpath()` + prefix-check for path traversal prevention
|
||||
- Admin forms require CSRF tokens via `AdminAuth::verifyCsrf()`
|
||||
- Passwords stored as bcrypt hashes in `admin.json`
|
||||
- **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/config/admin.json`
|
||||
- **Routing**: Via `?route=` parameter in `public/admin.php`
|
||||
- **Routes**: `login`, `logout`, `dashboard`, `content`, `content-edit`, `content-new`, `content-delete`, `content-dir-create`, `content-dir-rename`, `content-dir-delete`, `content-move`, `config`, `plugins`, `plugins-new`, `plugins-edit`, `plugins-config`, `plugins-toggle`, `plugins-delete`, `theme`, `theme-new`, `users`, `security`, `statistics`, `logs`, `guide`, `media`, `update`
|
||||
- **Auth**: Session-based. `AdminAuth` class handelt login, logout, CSRF, brute-force lockout af
|
||||
- **Templates**: Twig templates in `admin/theme/default/views/`. Layout in `layouts/admin.twig`
|
||||
|
||||
## 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!
|
||||
- **Examples**:
|
||||
- `nl.test.md` → display as "Test" (not content title)
|
||||
- `nl.test/` directory → display as "Test" (not H1 content)
|
||||
- `en.php-testen` → display as "Php Testen" (not "ICT")
|
||||
- **Method**: Use `formatDisplayName()` to process file/directory names correctly
|
||||
- **Priority**: Directory names take precedence over file names when both exist
|
||||
- **Language prefixes**: Dynamisch verwijderd op basis van beschikbare talen via `getAvailableLanguages()`
|
||||
|
||||
## Bekende aandachtspunten
|
||||
- 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, Twig, scssphp, GeoIP2). Niet handmatig wijzigen.
|
||||
- `admin/config/admin.json` bevat wachtwoord-hashes. Niet committen met echte productie-wachtwoorden.
|
||||
+4
-132
@@ -4,13 +4,13 @@
|
||||
|
||||
A lightweight, file-based content management system built with PHP (≥8.0).
|
||||
|
||||
**Version:** 2.6.1 | **License:** AGPL v3 / Commercial
|
||||
**Version:** 2.5.1 | **License:** AGPL v3 / Commercial
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 📝 **Multi-format Content** - Markdown, PHP and HTML files
|
||||
- 🧭 **Dynamic Navigation** - Automatic menu generation
|
||||
- 🌍 **Multi-language** - NL/EN/DE support
|
||||
- 🌍 **Multi-language** - NL/EN/DE/FR support
|
||||
- 🔍 **Search** - Full-text search
|
||||
- 📱 **Responsive** - Bootstrap 5 themes
|
||||
- 🔒 **Security** - 100/100 pentest score
|
||||
@@ -27,141 +27,13 @@ A lightweight, file-based content management system built with PHP (≥8.0).
|
||||
# Install dependencies
|
||||
composer install
|
||||
|
||||
# Start server with router for clean URLs (local only)
|
||||
# Start server with router for clean URLs
|
||||
php -S localhost:8080 cms/router.php
|
||||
```
|
||||
|
||||
**Website:** `http://localhost:8080`
|
||||
**Admin:** `http://localhost:8080/admin` (login: `admin` / `admin`)
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
- **PHP** ≥ 8.0 with extensions: `json`, `mbstring`
|
||||
- **Composer** (PHP dependency manager)
|
||||
- Web server: **Apache 2.4+** with `mod_rewrite` or **Nginx** with PHP-FPM
|
||||
- Optional: `opcache` (recommended for performance), `git` (for content versioning), `zip` extension (for ZIP backup/restore)
|
||||
|
||||
### Step 1 — Code and dependencies
|
||||
|
||||
```bash
|
||||
git clone <repository-url> codepress
|
||||
cd codepress
|
||||
composer install
|
||||
```
|
||||
|
||||
### Step 2 — Configuration
|
||||
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
cp admin/config/admin.json.example admin/config/admin.json
|
||||
```
|
||||
|
||||
Edit `config.json` with your site title, language and plugins. Change the admin password in `admin/config/admin.json` (default `admin`/`admin`).
|
||||
|
||||
### Step 3a — Apache 2.4+
|
||||
|
||||
The webroot is the `public/` directory. Example vhost (`/etc/apache2/sites-available/codepress.conf`):
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName example.com
|
||||
DocumentRoot /var/www/codepress/public
|
||||
|
||||
<Directory /var/www/codepress/public>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/codepress_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/codepress_access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Required Apache modules:
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite headers
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
- `mod_rewrite` — for clean URLs (`/nl/page`) and asset-serving
|
||||
- `mod_headers` — for security headers
|
||||
- `AllowOverride All` — so the `.htaccess` in `public/` is applied
|
||||
|
||||
### Step 3b — Nginx
|
||||
|
||||
Example server block (`/etc/nginx/sites-available/codepress`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
root /var/www/codepress/public;
|
||||
index index.php;
|
||||
|
||||
# Clean URLs: language-prefixed pages
|
||||
location ~ ^/(nl|en|de)(/(.+))?$ {
|
||||
try_files $uri /index.php?lang=$1&page=$2;
|
||||
}
|
||||
|
||||
# Admin routes
|
||||
location /admin {
|
||||
try_files $uri /admin.php?$args;
|
||||
}
|
||||
|
||||
# Asset-serving via asset.php (themes/plugins/admin outside webroot)
|
||||
location ~ ^/(themes|plugins)/([^/]+)/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
location ~ ^/admin/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
|
||||
# PHP via FPM
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php8.0-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# Security: block access to sensitive directories
|
||||
location ~ ^/(content|cms|admin/src|admin/config|admin/storage|var|vendor)/ {
|
||||
deny all;
|
||||
return 403;
|
||||
}
|
||||
|
||||
location ~ /\.(git|htaccess) {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Nginx does not use `.htaccess`. Security headers must be set in the Nginx config:
|
||||
|
||||
```nginx
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
|
||||
```
|
||||
|
||||
### Step 4 — Directory permissions
|
||||
|
||||
Make sure the web server has write access to the runtime directories:
|
||||
|
||||
```bash
|
||||
chown -R www-data:www-data var/ admin/storage/ content/
|
||||
chmod -R 755 .
|
||||
```
|
||||
|
||||
### Step 5 — Test
|
||||
|
||||
Open the website in your browser. With an empty content directory you'll see a welcome page. The admin console is available at `/admin` (login `admin`/`admin`).
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
See **[guide/](guide/)** for extensive documentation per role:
|
||||
@@ -196,7 +68,6 @@ codepress/
|
||||
├── admin/ # Admin console
|
||||
│ ├── config/ # Admin configuration (admin.json)
|
||||
│ ├── src/AdminAuth.php # Authentication, roles, permissions
|
||||
│ ├── static/ # Static files (404.html)
|
||||
│ ├── storage/ # Logs, cache, geoip
|
||||
│ └── theme/default/ # Admin theme
|
||||
│ ├── assets/ # CSS, JS, fonts, codemirror
|
||||
@@ -209,6 +80,7 @@ codepress/
|
||||
│ │ ├── *.twig # Layout templates
|
||||
│ │ ├── partials/ # Header, navigation, footer
|
||||
│ │ └── assets/ # SCSS, CSS, JS, img
|
||||
│ └── demo/ # Demo theme
|
||||
├── plugins/ # Plugins
|
||||
│ ├── HTMLBlock/ # Example sidebar plugin
|
||||
│ └── Navigation/ # Essential navigation plugin (protected)
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
Een lichtgewicht, file-based content management systeem gebouwd met PHP (≥8.0).
|
||||
|
||||
**Versie:** 2.6.1 | **Licentie:** AGPL v3 / Commercial
|
||||
**Versie:** 2.5.0 | **Licentie:** AGPL v3 / Commercial
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 📝 **Multi-format Content** - Markdown, PHP en HTML bestanden
|
||||
- 🧭 **Dynamic Navigation** - Automatische menu generatie
|
||||
- 🌍 **Multi-language** - NL/EN/DE ondersteuning
|
||||
- 🌍 **Multi-language** - NL/EN/DE/FR ondersteuning
|
||||
- 🔍 **Search** - Volledige tekst zoekfunctie
|
||||
- 📱 **Responsive** - Bootstrap 5 thema's
|
||||
- 🔒 **Security** - 100/100 pentest score
|
||||
@@ -27,141 +27,13 @@ Een lichtgewicht, file-based content management systeem gebouwd met PHP (≥8.0)
|
||||
# Installeer dependencies
|
||||
composer install
|
||||
|
||||
# Start server met router voor schone URLs (alleen lokaal)
|
||||
# Start server met router voor schone URLs
|
||||
php -S localhost:8080 cms/router.php
|
||||
```
|
||||
|
||||
**Website:** `http://localhost:8080`
|
||||
**Admin:** `http://localhost:8080/admin` (login: `admin` / `admin`)
|
||||
|
||||
## 📦 Installatie
|
||||
|
||||
### Vereisten
|
||||
|
||||
- **PHP** ≥ 8.0 met extensies: `json`, `mbstring`
|
||||
- **Composer** (PHP dependency manager)
|
||||
- Webserver: **Apache 2.4+** met `mod_rewrite` of **Nginx** met PHP-FPM
|
||||
- Optioneel: `opcache` (aanbevolen voor performance), `git` (voor content versioning), `zip` extensie (voor ZIP backup/restore)
|
||||
|
||||
### Stap 1 — Code en dependencies
|
||||
|
||||
```bash
|
||||
git clone <repository-url> codepress
|
||||
cd codepress
|
||||
composer install
|
||||
```
|
||||
|
||||
### Stap 2 — Configuratie
|
||||
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
cp admin/config/admin.json.example admin/config/admin.json
|
||||
```
|
||||
|
||||
Pas `config.json` aan met je site titel, taal en plugins. Wijzig het admin wachtwoord in `admin/config/admin.json` (standaard `admin`/`admin`).
|
||||
|
||||
### Stap 3a — Apache 2.4+
|
||||
|
||||
De webroot is de `public/` map. Voorbeeld vhost (`/etc/apache2/sites-available/codepress.conf`):
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName example.com
|
||||
DocumentRoot /var/www/codepress/public
|
||||
|
||||
<Directory /var/www/codepress/public>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/codepress_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/codepress_access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Benodigde Apache modules:
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite headers
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
- `mod_rewrite` — voor clean URLs (`/nl/pagina`) en asset-serving
|
||||
- `mod_headers` — voor security headers
|
||||
- `AllowOverride All` — zodat `.htaccess` in `public/` wordt toegepast
|
||||
|
||||
### Stap 3b — Nginx
|
||||
|
||||
Voorbeeld server block (`/etc/nginx/sites-available/codepress`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
root /var/www/codepress/public;
|
||||
index index.php;
|
||||
|
||||
# Clean URLs: taal-prefixed pagina's
|
||||
location ~ ^/(nl|en|de)(/(.+))?$ {
|
||||
try_files $uri /index.php?lang=$1&page=$2;
|
||||
}
|
||||
|
||||
# Admin routes
|
||||
location /admin {
|
||||
try_files $uri /admin.php?$args;
|
||||
}
|
||||
|
||||
# Asset-serving via asset.php (themes/plugins/admin buiten webroot)
|
||||
location ~ ^/(themes|plugins)/([^/]+)/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
location ~ ^/admin/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
|
||||
# PHP via FPM
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php8.0-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# Beveiliging: blokkeer toegang tot gevoelige mappen
|
||||
location ~ ^/(content|cms|admin/src|admin/config|admin/storage|var|vendor)/ {
|
||||
deny all;
|
||||
return 403;
|
||||
}
|
||||
|
||||
location ~ /\.(git|htaccess) {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Let op:** Nginx gebruikt geen `.htaccess`. De security headers moeten in de Nginx config worden gezet:
|
||||
|
||||
```nginx
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
|
||||
```
|
||||
|
||||
### Stap 4 — Mappen rechten
|
||||
|
||||
Zorg dat de webserver schrijfrechten heeft op de runtime mappen:
|
||||
|
||||
```bash
|
||||
chown -R www-data:www-data var/ admin/storage/ content/
|
||||
chmod -R 755 .
|
||||
```
|
||||
|
||||
### Stap 5 — Test
|
||||
|
||||
Open de website in je browser. Bij een lege content-map zie je een welkomstpagina. De admin console is bereikbaar via `/admin` (login `admin`/`admin`).
|
||||
|
||||
## 📚 Handleidingen
|
||||
|
||||
Zie **[guide/](guide/)** voor uitgebreide documentatie per rol:
|
||||
@@ -196,7 +68,6 @@ codepress/
|
||||
├── admin/ # Admin console
|
||||
│ ├── config/ # Admin configuratie (admin.json)
|
||||
│ ├── src/AdminAuth.php # Authenticatie, rollen, permissies
|
||||
│ ├── static/ # Statische bestanden (404.html)
|
||||
│ ├── storage/ # Logs, cache, geoip
|
||||
│ └── theme/default/ # Admin thema
|
||||
│ ├── assets/ # CSS, JS, fonts, codemirror
|
||||
@@ -209,16 +80,14 @@ codepress/
|
||||
│ │ ├── *.twig # Layout templates
|
||||
│ │ ├── partials/ # Header, navigation, footer
|
||||
│ │ └── assets/ # SCSS, CSS, JS, img
|
||||
│ └── demo/ # Demo thema
|
||||
├── plugins/ # Plugins
|
||||
│ ├── Dashboard/ # Systeem plugin (admin taal)
|
||||
│ ├── HTMLBlock/ # Content plugin (content taal)
|
||||
│ ├── Navigation/ # Essentiële navigatie plugin (beschermd)
|
||||
│ └── Statistics/ # Systeem plugin (admin taal)
|
||||
│ ├── Statistics.php # Plugin code
|
||||
│ ├── plugin.json # Plugin metadata + instellingen
|
||||
│ ├── README.md # Plugin documentatie
|
||||
│ ├── assets/ # Plugin CSS/JS/SCSS
|
||||
│ └── language/ # Plugin vertalingen (nl/, en/)
|
||||
│ ├── HTMLBlock/ # Voorbeeld sidebar plugin
|
||||
│ └── Navigation/ # Essentiële navigatie plugin (beschermd)
|
||||
│ ├── Navigation.php # Plugin code
|
||||
│ ├── plugin.json # Plugin metadata
|
||||
│ ├── assets/scss/ # Plugin SCSS bron
|
||||
│ └── assets/css/ # Plugin CSS
|
||||
├── content/ # Website content (.md, .php, .html)
|
||||
├── public/ # Web root
|
||||
│ ├── index.php # Website entry point
|
||||
@@ -284,33 +153,14 @@ codepress/
|
||||
|
||||
### Plugin structuur
|
||||
|
||||
Elke plugin heeft een uniforme structuur:
|
||||
|
||||
```
|
||||
plugins/MijnPlugin/
|
||||
├── MijnPlugin.php # Plugin code (naam = pluginnaam)
|
||||
├── plugin.json # Plugin metadata + instellingen
|
||||
├── README.md # Plugin documentatie
|
||||
├── plugin.json # Plugin metadata
|
||||
├── assets/scss/ # Plugin SCSS bron
|
||||
├── assets/css/ # Plugin CSS (na compilatie)
|
||||
└── language/ # Plugin vertalingen (i18n)
|
||||
├── nl/admin.php # NL admin labels (systeem plugins)
|
||||
└── en/admin.php # EN admin labels
|
||||
└── assets/css/ # Plugin CSS (na compilatie)
|
||||
```
|
||||
|
||||
- **Systeem plugins** volgen de admin-taal (`language/<lang>/admin.php`)
|
||||
- **Content plugins** volgen de content-taal (`language/<lang>/site.php`)
|
||||
- Fallback chain: geselecteerde taal → plugin `default_language` → CMS site default
|
||||
|
||||
Zie `guide/nl/codepress-developer/plugin-development.md` voor uitgebreide documentatie.
|
||||
|
||||
### Plugin editor
|
||||
|
||||
De admin plugin-editor (`/admin/plugins-edit`) biedt een volledige bestandsbeheer-omgeving:
|
||||
- Geneste bestandsbrowser zijbalk (alle bestanden in de plugin-map)
|
||||
- Nieuw bestand aanmaken, uploaden naar assets/, verwijderen en verplaatsen
|
||||
- CodeMirror editor voor .php, .json, .md, .html, .css, .scss, .js bestanden
|
||||
|
||||
### Essentiële plugins
|
||||
|
||||
De **Navigation** plugin is een essentiële plugin en kan niet worden gedeactiveerd, bewerkt of verwijderd. Deze plugin genereert automatisch de zijbalknavigatie voor handleidingen en content.
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
# TODO
|
||||
|
||||
## Voor elke versie verhoging. Deze nooit weghalen.
|
||||
- [x] Pentest controles uitgevoerd
|
||||
- [x] WCAG 2.1 AA accessibility tests
|
||||
- [x] Volledige git commit maken.
|
||||
- [x] Verslag maken voor opdrcht gever
|
||||
- [x] Na convormatie versie verhogen
|
||||
- [x] Bij Voltooid versie ophoging aanmaken en deze allemaal unvinken voor volgende ronde.
|
||||
- [ ] Pentest controles uitgevoerd
|
||||
- [ ] WCAG 2.1 AA accessibility tests
|
||||
- [ ] Volledige git commit maken.
|
||||
- [ ] Verslag maken voor opdrcht gever
|
||||
- [ ] Na convormatie versie verhogen
|
||||
- [ ] Bij Voltooid versie ophoging aanmaken en deze allemaal unvinken voor volgende ronde.
|
||||
|
||||
## Bug fixes
|
||||
- [x] admin rollen werkt niet goed, dashboard geeft een 404, de plugin's moeten aangege welke rol nodig is.
|
||||
- [x] user admin mag nooit zijn eigen rol weizigen.
|
||||
- [x] Na in loggen moet de admin zijn rol binnen de /admin kunnen veranderen om andere rollen te kunnen testen
|
||||
- [x] in een andere rol kan de gebruiker niet zijn eigen wachtwoord veranderen.
|
||||
- [x] Bij het aanmaken van een pagina in de content moet de auteur naam en email in de meta komen te staan. Deze moeten dus ook door middel van de api aan de twig bestanden doorgeven kunnen worden
|
||||
- [x] Admin taal niet volledig geïntegreerd met plugins: plugins hebben nu eigen language/ mappen (nl/en). Systeem plugins volgen admin taal (admin.php), content plugins volgen content taal (site.php). Fallback chain: geselecteerd → plugin default_language → CMS default → leeg. PluginManager + AdminPluginAPI + CMSAPI uitgebreid met getPluginTranslations()/t(). plugin.json settings ondersteunen label_key/help_key/option_label_key.
|
||||
- [x] Plugin uniformiteit: alle 6 plugins hebben nu een uniforme structuur (<Plugin>.php, plugin.json, README.md, assets/.gitkeep, language/nl|en/). plugins/README.md herschreven. guide plugin-development.md (NL+EN) volledig bijgewerkt met i18n + admin integratie + instellingen-schema. guide architectuur.md (NL+EN) bijgewerkt met plugin language/ map.
|
||||
- [x] Plugin editor toont alleen de <Plugin>.php maar een plugin kan uit meerdere bestanden bestaan: plugins-edit pagina heeft nu een bestandsbrowser zijbalk die alle bestanden in de plugin-map toont (.php, .json, .md, .html, .css, .scss, .js) inclusief language/ en assets/ submappen. Nieuw-bestand knop met modal. Path-traversal bescherming via realpath + prefix check + segment-guard. scanPluginFiles() helper toegevoegd. editor-toolbar.js modeMap uitgebreid voor css/scss/js/json. Vertaalstrings toegevoegd aan nl/en/de admin.php.
|
||||
- [x] Plugin editor uploaden en verwijderen: plugins-edit pagina heeft nu een Upload knop (bestanden naar assets/ van de plugin) en per-bestand verwijder-knop in de bestandsboom. Twee nieuwe routes: plugins-file-upload en plugins-file-delete. Path-traversal bescherming + protected plugins geblokkeerd + dotfiles geweigerd. Vertaalstrings toegevoegd.
|
||||
- [x] Plugin editor bestand verplaatsen: plugins-edit pagina heeft nu per-bestand een verplaats-knop in de bestandsboom. Nieuwe route plugins-file-move + template plugins-move-form.twig (dropdown met mappen binnen de plugin, plugin root als optie, huidige map uitgesloten). Path-traversal bescherming + protected plugins geblokkeerd.
|
||||
## Te doen ⏳
|
||||
|
||||
|
||||
## Nice to have
|
||||
- [ ] Multidomein implementeren (elk domein = eigen thema + content, één admin + site name)
|
||||
- [ ] Fase 1: Config-resolutie
|
||||
- [ ] For apache instants domein settings /public/noorlander.info/ or /public/mycode.name/
|
||||
@@ -61,36 +49,6 @@
|
||||
- [x] Pentest controles uitgevoerd (30/30 tests geslaagd — 0 vulnerabilities)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25 tests geslaagd — 100% compliance, test-script verbeterd met min/max checks en grep -E)
|
||||
|
||||
## v2.6.1d (2026-08-18) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
- [x] Plugin internationalisatie (i18n): plugins hebben eigen language/ mappen, systeem plugins volgen admin taal, content plugins volgen content taal, fallback chain
|
||||
- [x] Plugin uniformiteit: alle 6 plugins hebben uniforme structuur (README.md, assets/.gitkeep, language/nl|en/)
|
||||
- [x] Plugin editor vernieuwd: bestandsbrowser zijbalk (geneste boom), nieuw bestand aanmaken, uploaden, verwijderen, verplaatsen
|
||||
- [x] Media invoegen in editor: nieuw /admin/media-list JSON endpoint + herbruikbare _media-modal.twig include; plugin-context scant assets/ map
|
||||
- [x] Plugin overzicht knoppen: alleen iconen met title/aria-label
|
||||
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt (plugin-development.md NL+EN volledig herschreven, architectuur.md NL+EN bijgewerkt)
|
||||
- [x] Verslag gemaakt (docs/release-notes/v2.6.1d.md)
|
||||
- [x] Versie verhoogd naar 2.6.1d
|
||||
|
||||
## v2.6.1c (2026-08-17) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt
|
||||
- [x] Admin gebruikersbeheer opnieuw ontworpen (lijst + zoeken/filter + profiel + wachtwoord)
|
||||
- [x] CLI commando voor admin wachtwoord reset (cli/reset-admin-password.php)
|
||||
- [x] Verslag gemaakt (docs/release-notes/v2.6.1c.md)
|
||||
- [x] Versie verhoogd naar 2.6.1c
|
||||
|
||||
## v2.6.1 (2026-08-17) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt
|
||||
- [x] Installatie instructies toegevoegd aan README (Apache2/Nginx/PHP/composer)
|
||||
- [x] Verwijderde vendor packages: php-mqtt/client, mustache/mustache (uit composer + autoloader)
|
||||
- [x] Verslag gemaakt (docs/release-notes/v2.6.1.md)
|
||||
- [x] Versie verhoogd naar 2.6.1
|
||||
|
||||
## v2.6.0 (2026-08-15) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"password_hash": "$2y$12$CV2uORTIiie6s3jf9ANaneL7VLNqA5smXyrIzkcJFjlS2Vxfw1/HK",
|
||||
"role": "admin",
|
||||
"email": "",
|
||||
"author_name": "",
|
||||
"author_email": "",
|
||||
"created": "2025-01-01"
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"session_timeout": 1800,
|
||||
"max_login_attempts": 5,
|
||||
"lockout_duration": 900
|
||||
},
|
||||
"config": {
|
||||
"default_page": "auto"
|
||||
}
|
||||
}
|
||||
@@ -187,71 +187,12 @@ class AdminAuth
|
||||
|
||||
/**
|
||||
* Get the role of the current user.
|
||||
* Returns the override role if set (for testing), otherwise the real role.
|
||||
*/
|
||||
public function getCurrentRole(): string
|
||||
{
|
||||
if (isset($_SESSION['admin_role_override'])) {
|
||||
return $_SESSION['admin_role_override'];
|
||||
}
|
||||
return $_SESSION['admin_role'] ?? 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real role of the current user (ignoring any override).
|
||||
*/
|
||||
public function getRealRole(): string
|
||||
{
|
||||
return $_SESSION['admin_role'] ?? 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has an active role override.
|
||||
*/
|
||||
public function hasRoleOverride(): bool
|
||||
{
|
||||
return isset($_SESSION['admin_role_override']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily switch to another role for testing purposes.
|
||||
* Only admin users can do this; the override cannot grant more than admin.
|
||||
*/
|
||||
public function switchRole(string $role): array
|
||||
{
|
||||
if (!$this->isAuthenticated()) {
|
||||
return ['success' => false, 'message' => 'Niet ingelogd.'];
|
||||
}
|
||||
// Only real admins can switch roles
|
||||
$realRole = $this->getRealRole();
|
||||
if ($realRole !== 'admin') {
|
||||
return ['success' => false, 'message' => 'Alleen admins kunnen van rol wisselen.'];
|
||||
}
|
||||
if (!isset(self::ROLE_PERMISSIONS[$role])) {
|
||||
return ['success' => false, 'message' => 'Ongeldige rol.'];
|
||||
}
|
||||
// Admins cannot switch to 'admin' (no point) — only to lower roles for testing
|
||||
if ($role === 'admin') {
|
||||
return ['success' => false, 'message' => 'Je bent al admin.'];
|
||||
}
|
||||
$_SESSION['admin_role_override'] = $role;
|
||||
$this->log('info', "Rol-switch: {$_SESSION['admin_user']} -> {$role} (test)");
|
||||
return ['success' => true, 'message' => 'Rol gewijzigd naar ' . self::getRoleLabel($role) . '.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the role override back to the real admin role.
|
||||
*/
|
||||
public function resetRole(): array
|
||||
{
|
||||
if (isset($_SESSION['admin_role_override'])) {
|
||||
unset($_SESSION['admin_role_override']);
|
||||
$this->log('info', "Rol-switch gereset: {$_SESSION['admin_user']}");
|
||||
return ['success' => true, 'message' => 'Rol teruggezet naar admin.'];
|
||||
}
|
||||
return ['success' => false, 'message' => 'Geen rol-override actief.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has permission to access a route.
|
||||
*/
|
||||
@@ -367,13 +308,9 @@ class AdminAuth
|
||||
|
||||
/**
|
||||
* Change the role of an existing user.
|
||||
* A user can never change their own role (security guard).
|
||||
*/
|
||||
public function changeRole(string $username, string $role): array
|
||||
{
|
||||
if ($username === ($_SESSION['admin_user'] ?? '')) {
|
||||
return ['success' => false, 'message' => 'Je kunt je eigen rol niet wijzigen.'];
|
||||
}
|
||||
if (!isset(self::ROLE_PERMISSIONS[$role])) {
|
||||
return ['success' => false, 'message' => 'Ongeldige rol.'];
|
||||
}
|
||||
@@ -495,14 +432,6 @@ class AdminAuth
|
||||
file_put_contents($this->lockFile, json_encode($attempts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all failed login attempts for a user (public, used by CLI reset).
|
||||
*/
|
||||
public function clearLockout(string $username): void
|
||||
{
|
||||
$this->clearFailedAttempts($username);
|
||||
}
|
||||
|
||||
private function getFailedAttempts(): array
|
||||
{
|
||||
if (!file_exists($this->lockFile)) {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<div class="error-page text-center py-5">
|
||||
<div class="error-code display-1 fw-bold text-primary mb-3">404</div>
|
||||
<h1 class="h2 mb-3">Pagina niet gevonden</h1>
|
||||
<p class="text-muted mb-4">De pagina die u zoekt bestaat niet of is verplaatst.</p>
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="bi bi-house me-1" aria-hidden="true"></i>
|
||||
Terug naar de hoofdpagina
|
||||
</a>
|
||||
</div>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 31 KiB |
@@ -7,7 +7,7 @@
|
||||
var ext = textarea.dataset.ext || 'md';
|
||||
var form = document.getElementById('editor-form') || textarea.closest('form');
|
||||
|
||||
var modeMap = { md: 'markdown', html: 'htmlmixed', php: 'php', css: 'css', scss: 'css', js: 'javascript', json: 'javascript' };
|
||||
var modeMap = { md: 'markdown', html: 'htmlmixed', php: 'php' };
|
||||
|
||||
var editor = CodeMirror.fromTextArea(textarea, {
|
||||
mode: modeMap[ext] || 'markdown',
|
||||
@@ -146,11 +146,8 @@
|
||||
insert(editor, '/**\n * \n */', 7);
|
||||
break;
|
||||
case 'media':
|
||||
var modalEl = document.getElementById('mediaModal');
|
||||
if (modalEl && window.bootstrap) {
|
||||
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
modal.show();
|
||||
}
|
||||
var modal = new bootstrap.Modal(document.getElementById('mediaModal'));
|
||||
if (modal) modal.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,21 +40,18 @@
|
||||
<i class="bi bi-gear-fill"></i> {{ ta.admin_title|default('CodePress Admin') }}
|
||||
</div>
|
||||
<ul class="nav flex-column mt-2">
|
||||
{# Algemene sectie: Dashboard (altijd zichtbaar) + plugin items met section=general #}
|
||||
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general' and item.route != 'dashboard' and (item.permission is not defined or item.permission == 'dashboard' or has_permission(item.permission)) and (item.required_roles is not defined or user_role == 'admin' or user_role in item.required_roles)) %}
|
||||
{# Algemene sectie: Dashboard (plugin) + plugin items met section=general #}
|
||||
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general') %}
|
||||
{% if general_plugins is not empty or has_permission('dashboard') or true %}
|
||||
<li class="nav-section">{{ ta.section_general|default('Algemeen') }}</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ route == 'dashboard' or route == '' ? 'active' : '' }}" href="/admin/dashboard">
|
||||
<i class="bi bi-speedometer2"></i> {{ ta.dashboard|default('Dashboard') }}
|
||||
</a>
|
||||
</li>
|
||||
{% for item in general_plugins %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
|
||||
<i class="bi {{ item.icon|default('bi-puzzle') }}"></i> {{ plugin_menu_label(item) }}
|
||||
<i class="bi {{ item.icon|default('bi-puzzle') }}"></i> {{ item.label|default(item.route) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if has_permission('content') %}
|
||||
<li class="nav-section">{{ ta.section_content|default('Content') }}</li>
|
||||
@@ -91,7 +88,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Systeem sectie: core admin items + plugin items met section=system #}
|
||||
{% set system_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'system' and (item.permission is not defined or item.permission == 'dashboard' or has_permission(item.permission)) and (item.required_roles is not defined or user_role == 'admin' or user_role in item.required_roles)) %}
|
||||
{% set system_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'system') %}
|
||||
{% if has_permission('plugins') or has_permission('users') or has_permission('update') or system_plugins is not empty %}
|
||||
<li class="nav-section">{{ ta.section_system|default('Systeem') }}</li>
|
||||
{% if has_permission('plugins') %}
|
||||
@@ -118,7 +115,7 @@
|
||||
{% for item in system_plugins %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
|
||||
<i class="bi {{ item.icon|default('bi-puzzle') }}"></i> {{ plugin_menu_label(item) }}
|
||||
<i class="bi {{ item.icon|default('bi-puzzle') }}"></i> {{ item.label|default(item.route) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -135,18 +132,6 @@
|
||||
</ul>
|
||||
|
||||
<ul class="nav flex-column mt-auto mb-5">
|
||||
{% if user_real_role == 'admin' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#roleSwitchModal">
|
||||
<i class="bi bi-person-bounding-box"></i>
|
||||
{% if has_role_override %}
|
||||
{{ ta.role_testing|default('Testen') }}: {{ role_label(user_role) }}
|
||||
{% else %}
|
||||
{{ ta.role_switch|default('Rol wisselen') }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/" target="_blank">
|
||||
<i class="bi bi-box-arrow-up-right"></i> {{ ta.view_website|default('Website bekijken') }}
|
||||
@@ -160,54 +145,10 @@
|
||||
</ul>
|
||||
<div class="admin-user">
|
||||
<i class="bi bi-person-circle"></i> {{ user.username|default('') }}
|
||||
<br><small>{{ role_label(user_role)|default('') }}{% if has_role_override %} <span class="badge bg-warning text-dark">{{ ta.role_testing|default('Test') }}</span>{% endif %}</small>
|
||||
<br><small>{{ role_label(user_role)|default('') }}</small>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{# Role switch modal (only rendered for real admins) #}
|
||||
{% if user_real_role == 'admin' %}
|
||||
<div class="modal fade" id="roleSwitchModal" tabindex="-1" aria-labelledby="roleSwitchModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/{% if has_role_override %}role-reset{% else %}role-switch{% endif %}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="roleSwitchModalLabel">
|
||||
<i class="bi bi-person-bounding-box"></i>
|
||||
{% if has_role_override %}{{ ta.role_reset_title|default('Rol terugzetten') }}{% else %}{{ ta.role_switch_title|default('Rol wisselen') }}{% endif %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if has_role_override %}
|
||||
<p>{{ ta.role_override_active|default('Je test momenteel als') }}: <strong>{{ role_label(user_role) }}</strong></p>
|
||||
<p class="text-muted small">{{ ta.role_reset_help|default('Klik hieronder om terug te keren naar je admin rol.') }}</p>
|
||||
{% else %}
|
||||
<p>{{ ta.role_switch_help|default('Test de admin vanuit een andere rol. Je echte account blijft admin.') }}</p>
|
||||
<div class="mb-3">
|
||||
<label for="role_switch_select" class="form-label">{{ ta.new_role|default('Nieuwe rol') }}</label>
|
||||
<select class="form-select" id="role_switch_select" name="new_role">
|
||||
{% for roleKey, roleLabel in roles|default([]) %}
|
||||
{% if roleKey != 'admin' %}
|
||||
<option value="{{ roleKey }}">{{ roleLabel }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if has_role_override %}<i class="bi bi-arrow-counterclockwise"></i> {{ ta.role_reset_btn|default('Terug naar admin') }}{% else %}<i class="bi bi-check-lg"></i> {{ ta.role_switch_btn|default('Wissel naar rol') }}{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<main class="admin-main">
|
||||
{% if message is defined and message %}
|
||||
<div class="alert alert-{{ message_type|default('info') }} alert-dismissible fade show" role="alert">
|
||||
@@ -219,10 +160,6 @@
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% if needs_editor %}
|
||||
{% include 'pages/_media-modal.twig' %}
|
||||
{% endif %}
|
||||
|
||||
<script src="/admin/assets/js/bootstrap.bundle.min.js"></script>
|
||||
{% if needs_editor %}
|
||||
<script src="/admin/assets/codemirror/codemirror.min.js"></script>
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
{# Reusable media modal, included by admin.twig when needs_editor is true.
|
||||
The editor toolbar "media" button opens this modal. It fetches the list
|
||||
of media files from /admin/media-list (JSON) and, on click, inserts a
|
||||
snippet into the active CodeMirror editor at the cursor.
|
||||
|
||||
Scope:
|
||||
- Default (content editor): fetches /admin/media-list
|
||||
- Plugin editor (mediaPluginName set): fetches /admin/media-list?plugin=<name>
|
||||
which scans plugins/<name>/assets/ instead of content/.
|
||||
|
||||
The snippet format depends on the editor's current mode (data-ext):
|
||||
- markdown (md): 
|
||||
- html/php: <img src="url" alt="name">
|
||||
- other (css, json, js, ...): the raw URL #}
|
||||
<div class="modal fade" id="mediaModal" tabindex="-1" aria-labelledby="mediaModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="mediaModalLabel"><i class="bi bi-images"></i> {{ ta.media_insert|default('Media invoegen') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group input-group-sm mb-3">
|
||||
<span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span>
|
||||
<input type="search" id="mediaModalFilter" class="form-control" placeholder="{{ ta.media_filter|default('Filter op bestandsnaam...') }}" autocomplete="off" aria-label="{{ ta.media_filter|default('Filter') }}">
|
||||
</div>
|
||||
<div id="mediaModalGrid" class="row g-3">
|
||||
<div class="col-12 text-center text-muted py-4" id="mediaModalLoading">
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
<span class="ms-2">{{ ta.media_loading|default('Media laden...') }}</span>
|
||||
</div>
|
||||
<div class="col-12 text-center text-muted py-4 d-none" id="mediaModalEmpty">
|
||||
<i class="bi bi-image display-6 d-block mb-2"></i>
|
||||
{{ mediaEmptyText|default('Geen media bestanden gevonden in content/. Upload eerst bestanden via Media in het menu.') }}
|
||||
</div>
|
||||
<div class="col-12 text-center text-danger py-4 d-none" id="mediaModalError">
|
||||
<i class="bi bi-exclamation-triangle display-6 d-block mb-2"></i>
|
||||
<span id="mediaModalErrorText"></span>
|
||||
</div>
|
||||
{# Filled by JS #}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var loaded = false;
|
||||
var items = [];
|
||||
|
||||
function formatSnippet(url, name, ext, mode) {
|
||||
if (mode === 'markdown') {
|
||||
return '![' + name.replace(/[\[\]]/g, '') + '](' + url + ')';
|
||||
}
|
||||
if (mode === 'html' || mode === 'htmlmixed' || mode === 'php') {
|
||||
var imgExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
||||
if (imgExts.indexOf(ext) !== -1) {
|
||||
return '<img src="' + url + '" alt="' + name.replace(/"/g, '"') + '">';
|
||||
}
|
||||
if (ext === 'pdf') {
|
||||
return '<a href="' + url + '">' + name + '</a>';
|
||||
}
|
||||
if (['mp4', 'webm', 'mov', 'ogg'].indexOf(ext) !== -1) {
|
||||
return '<video src="' + url + '" controls></video>';
|
||||
}
|
||||
if (['mp3', 'wav'].indexOf(ext) !== -1) {
|
||||
return '<audio src="' + url + '" controls></audio>';
|
||||
}
|
||||
return '<a href="' + url + '">' + name + '</a>';
|
||||
}
|
||||
// css, scss, js, json, etc.: just insert the URL
|
||||
return url;
|
||||
}
|
||||
|
||||
function modeForExt(ext) {
|
||||
var map = { md: 'markdown', html: 'htmlmixed', php: 'php', css: 'css', scss: 'css', js: 'javascript', json: 'javascript' };
|
||||
return map[ext] || 'markdown';
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
var grid = document.getElementById('mediaModalGrid');
|
||||
if (!grid) return;
|
||||
|
||||
// Clear previous items (keep loading/empty/error placeholders)
|
||||
Array.prototype.forEach.call(grid.querySelectorAll('[data-media-item]'), function (el) { el.remove(); });
|
||||
|
||||
var q = (document.getElementById('mediaModalFilter').value || '').trim().toLowerCase();
|
||||
var shown = 0;
|
||||
|
||||
items.forEach(function (item) {
|
||||
if (q && item.name.toLowerCase().indexOf(q) === -1) return;
|
||||
shown++;
|
||||
|
||||
var col = document.createElement('div');
|
||||
col.className = 'col-6 col-md-4 col-lg-3';
|
||||
col.setAttribute('data-media-item', '1');
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'card shadow-sm h-100';
|
||||
card.style.cursor = 'pointer';
|
||||
card.title = item.name + ' (' + item.size + ')';
|
||||
|
||||
if (item.is_image) {
|
||||
var img = document.createElement('img');
|
||||
img.src = item.url;
|
||||
img.className = 'card-img-top';
|
||||
img.style.objectFit = 'cover';
|
||||
img.style.height = '120px';
|
||||
img.loading = 'lazy';
|
||||
img.alt = item.name;
|
||||
card.appendChild(img);
|
||||
} else {
|
||||
var ph = document.createElement('div');
|
||||
ph.className = 'card-img-top d-flex align-items-center justify-content-center bg-light';
|
||||
ph.style.height = '120px';
|
||||
var icon = document.createElement('i');
|
||||
icon.className = 'bi bi-file-earmark-play display-6 text-muted';
|
||||
ph.appendChild(icon);
|
||||
card.appendChild(ph);
|
||||
}
|
||||
|
||||
var body = document.createElement('div');
|
||||
body.className = 'card-body p-2';
|
||||
var label = document.createElement('p');
|
||||
label.className = 'card-text small text-muted text-truncate mb-0';
|
||||
label.textContent = item.name;
|
||||
body.appendChild(label);
|
||||
card.appendChild(body);
|
||||
|
||||
card.addEventListener('click', function () {
|
||||
var editor = window.codeMirrorEditor;
|
||||
if (!editor) { return; }
|
||||
var ext = (document.getElementById('editor-textarea') || {}).dataset;
|
||||
var fileExt = (ext && ext.ext) ? ext.ext : 'md';
|
||||
var mode = modeForExt(fileExt);
|
||||
var snippet = formatSnippet(item.url, item.name, item.extension, mode);
|
||||
editor.replaceSelection(snippet);
|
||||
editor.focus();
|
||||
// Close the modal
|
||||
var modalEl = document.getElementById('mediaModal');
|
||||
if (window.bootstrap && modalEl) {
|
||||
var inst = bootstrap.Modal.getInstance(modalEl);
|
||||
if (inst) inst.hide();
|
||||
}
|
||||
});
|
||||
|
||||
col.appendChild(card);
|
||||
grid.appendChild(col);
|
||||
});
|
||||
|
||||
// Toggle empty placeholder
|
||||
var empty = document.getElementById('mediaModalEmpty');
|
||||
var loading = document.getElementById('mediaModalLoading');
|
||||
if (loading) loading.classList.add('d-none');
|
||||
if (empty) empty.classList.toggle('d-none', shown > 0);
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (loaded) { renderGrid(); return; }
|
||||
var plugin = {{ mediaPluginName|default('')|json_encode|raw }};
|
||||
var url = '/admin/media-list';
|
||||
if (plugin) { url += '?plugin=' + encodeURIComponent(plugin); }
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
items = Array.isArray(data) ? data : [];
|
||||
loaded = true;
|
||||
renderGrid();
|
||||
})
|
||||
.catch(function (err) {
|
||||
var loading = document.getElementById('mediaModalLoading');
|
||||
if (loading) loading.classList.add('d-none');
|
||||
var errEl = document.getElementById('mediaModalError');
|
||||
var errTxt = document.getElementById('mediaModalErrorText');
|
||||
if (errEl) errEl.classList.remove('d-none');
|
||||
if (errTxt) errTxt.textContent = String(err);
|
||||
});
|
||||
}
|
||||
|
||||
// Load when the modal is first shown
|
||||
var modalEl = document.getElementById('mediaModal');
|
||||
if (modalEl) {
|
||||
modalEl.addEventListener('show.bs.modal', load);
|
||||
}
|
||||
// Filter as the user types
|
||||
var filter = document.getElementById('mediaModalFilter');
|
||||
if (filter) {
|
||||
filter.addEventListener('input', renderGrid);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -40,22 +40,6 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if isEditable and (currentAuthorName or currentAuthorEmail or currentCreated) %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted"><i class="bi bi-person"></i> {{ ta.author_name|default('Auteur naam') }}</label>
|
||||
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentAuthorName }}" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted"><i class="bi bi-envelope"></i> {{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentAuthorEmail }}" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted"><i class="bi bi-calendar"></i> {{ ta.created|default('Aangemaakt') }}</label>
|
||||
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentCreated }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if isEditable %}
|
||||
<div class="editor-toolbar" id="editor-toolbar"></div>
|
||||
<div class="editor-wrapper">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ route|capitalize }} - CodePress Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ plugin_content|raw }}
|
||||
{% endblock %}
|
||||
@@ -3,98 +3,18 @@
|
||||
{% block title %}{{ ta.plugin_config|default('Plugin Configuratie: ') }}{{ pluginName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-gear"></i> {{ ta.plugin_config|default('Plugin Configuratie: ') }}{{ pluginName }}</h2>
|
||||
<a href="/admin/plugins" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="mb-4"><i class="bi bi-plug"></i> {{ ta.plugin_config|default('Plugin Configuratie: ') }}{{ pluginName }}</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-sliders"></i> {{ ta.plugin_settings|default('Instellingen') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if settingsSchema is empty %}
|
||||
<p class="text-muted mb-0">{{ ta.plugin_no_settings|default('Deze plugin heeft geen instelbare configuratie.') }}</p>
|
||||
{% else %}
|
||||
<form method="POST" action="/admin/plugins-config?plugin={{ pluginName|url_encode }}">
|
||||
<form method="post" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
{% for setting in settingsSchema %}
|
||||
{% set settingLabel = setting.resolved_label|default('') ? setting.resolved_label : (setting.label|default(setting.key)) %}
|
||||
{% set settingHelp = setting.resolved_help|default('') ? setting.resolved_help : (setting.help|default('')) %}
|
||||
{% set settingOptions = setting.resolved_options is not null ? setting.resolved_options : (setting.options|default([])) %}
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold" for="setting-{{ setting.key }}">{{ settingLabel }}</label>
|
||||
{% set currentValue = resolvedConfig[setting.key]|default(setting.default) %}
|
||||
{% if setting.type == 'select' %}
|
||||
<select class="form-select" id="setting-{{ setting.key }}" name="setting_{{ setting.key }}">
|
||||
{% for optValue, optLabel in settingOptions %}
|
||||
<option value="{{ optValue }}" {{ currentValue == optValue ? 'selected' : '' }}>{{ optLabel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elseif setting.type == 'multi-select' %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for optValue, optLabel in settingOptions %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="setting-{{ setting.key }}-{{ optValue }}" name="setting_{{ setting.key }}[]" value="{{ optValue }}" {{ optValue in currentValue ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="setting-{{ setting.key }}-{{ optValue }}">{{ optLabel }}</label>
|
||||
<div class="card-body">
|
||||
<textarea name="config" id="config-textarea" class="form-control font-monospace" rows="20">{{ pluginConfig }}</textarea>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elseif setting.type == 'checkbox' %}
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="setting-{{ setting.key }}" name="setting_{{ setting.key }}" {{ currentValue ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="setting-{{ setting.key }}">{{ settingLabel }}</label>
|
||||
</div>
|
||||
{% elseif setting.type == 'number' %}
|
||||
<input type="number" class="form-control" id="setting-{{ setting.key }}" name="setting_{{ setting.key }}" value="{{ currentValue }}" min="1">
|
||||
{% else %}
|
||||
<input type="text" class="form-control" id="setting-{{ setting.key }}" name="setting_{{ setting.key }}" value="{{ currentValue }}">
|
||||
{% endif %}
|
||||
{% if settingHelp %}
|
||||
<small class="form-text text-muted">{{ settingHelp }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="d-flex gap-2">
|
||||
<div class="card-footer bg-white">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
<a href="/admin/plugins" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle"></i> {{ ta.plugin_info|default('Plugin informatie') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tr><td class="text-muted">{{ ta.plugin_name|default('Naam') }}</td><td>{{ pluginJson.name|default(pluginName) }}</td></tr>
|
||||
<tr><td class="text-muted">{{ ta.version|default('Versie') }}</td><td>{{ pluginJson.version|default('-') }}</td></tr>
|
||||
<tr><td class="text-muted">{{ ta.author|default('Auteur') }}</td><td>{{ pluginJson.author|default('-') }}</td></tr>
|
||||
<tr><td class="text-muted">{{ ta.type|default('Type') }}</td><td><span class="badge bg-{{ pluginJson.type == 'system' ? 'primary' : 'success' }}">{{ pluginJson.type|default('content') }}</span></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% if pluginJson.description %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-card-text"></i> {{ ta.description|default('Beschrijving') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">{{ pluginJson.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -2,183 +2,26 @@
|
||||
|
||||
{% block title %}{{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{# Recursively render the plugin file tree as a nested, collapsible list.
|
||||
A folder is expanded by default when the currently selected file lives
|
||||
somewhere inside it, so the user always sees where their file is. #}
|
||||
{% macro tree(nodes, pluginName, relFile, editableExts, csrfToken) %}
|
||||
{% import _self as macros %}
|
||||
<ul class="plugin-tree">
|
||||
{% for n in nodes %}
|
||||
{% set isActive = n.path == relFile %}
|
||||
{# A folder contains the active file when relFile starts with n.path + '/' #}
|
||||
{% set containsActive = n.is_dir and relFile starts with (n.path ~ '/') %}
|
||||
{# Icon per type #}
|
||||
{% set icon = n.is_dir ? 'bi-folder-fill text-warning' : (n.extension == 'php' ? 'bi-file-code text-success' : (n.extension == 'json' ? 'bi-filetype-json text-secondary' : (n.extension == 'md' ? 'bi-file-text text-primary' : (n.extension in ['css','scss'] ? 'bi-filetype-css text-info' : (n.extension == 'js' ? 'bi-filetype-js text-warning' : (n.extension == 'html' ? 'bi-file-earmark text-info' : 'bi-file text-muted')))))) %}
|
||||
<li>
|
||||
{% if n.is_dir %}
|
||||
{% set collapseId = 'plugin-tree-' ~ n.path|replace({'/':'-','.':'-'}) %}
|
||||
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ collapseId }}"
|
||||
aria-expanded="{{ containsActive ? 'true' : 'false' }}"
|
||||
title="{{ n.path }}">
|
||||
<i class="bi bi-chevron-down tree-chevron"></i>
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
</button>
|
||||
<div id="{{ collapseId }}"
|
||||
class="tree-collapse collapse {{ containsActive ? 'show' : '' }}">
|
||||
{% if n.children is not empty %}
|
||||
{{ macros.tree(n.children, pluginName, relFile, editableExts, csrfToken) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/admin/plugins-edit?plugin={{ pluginName|url_encode }}&file={{ n.path|url_encode }}"
|
||||
class="tree-link {{ isActive ? 'is-active' : '' }}"
|
||||
title="{{ n.path }}">
|
||||
<span class="tree-chevron-spacer"></span>
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
{% if n.extension in editableExts %}
|
||||
<span class="badge bg-secondary badge-ext">{{ n.extension|upper }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/admin/plugins-file-move?plugin={{ pluginName|url_encode }}&file={{ n.path|url_encode }}"
|
||||
class="tree-move-btn" title="{{ ta.move|default('Verplaatsen') }}" aria-label="{{ ta.move|default('Verplaatsen') }}">
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/plugins-file-delete" class="tree-delete-form" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrfToken }}">
|
||||
<input type="hidden" name="plugin" value="{{ pluginName }}">
|
||||
<input type="hidden" name="file" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm text-danger p-0 ms-1 tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
{% import _self as tree_macros %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
|
||||
<link rel="stylesheet" href="/admin/assets/css/editor.css">
|
||||
<style>
|
||||
.plugin-editor-layout { display: flex; gap: 1rem; align-items: stretch; }
|
||||
.plugin-file-tree { width: 280px; flex: 0 0 280px; }
|
||||
.plugin-editor-main { flex: 1 1 auto; min-width: 0; }
|
||||
.plugin-tree { list-style: none; padding-left: 0; margin: 0; }
|
||||
.plugin-tree ul { list-style: none; padding-left: 1.1rem; margin: 0; }
|
||||
.plugin-tree li { padding: 0; position: relative; }
|
||||
.plugin-tree li > .tree-link { display: flex; align-items: center; gap: .3rem; padding: .2rem .4rem; border-radius: .25rem; text-decoration: none; color: inherit; font-size: .9rem; line-height: 1.4; }
|
||||
.plugin-tree li > .tree-link:hover { background-color: rgba(13,110,253,.08); }
|
||||
.plugin-tree li > .tree-link.is-active { background-color: var(--bs-primary-bg-subtle, #cfe2ff); font-weight: 600; }
|
||||
.plugin-tree .tree-toggle { cursor: pointer; user-select: none; width: 100%; text-align: left; background: transparent; border: 0; color: inherit; }
|
||||
.plugin-tree .tree-chevron { transition: transform .15s ease; flex: 0 0 auto; }
|
||||
.plugin-tree .tree-chevron-spacer { display: inline-block; width: .9rem; flex: 0 0 auto; }
|
||||
.plugin-tree .btn-toggle.collapsed .tree-chevron { transform: rotate(-90deg); }
|
||||
.plugin-tree .badge-ext { font-size: .6rem; margin-left: auto; }
|
||||
.plugin-tree .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
/* Move and delete buttons: always visible, right-aligned, normal icon size */
|
||||
.plugin-tree .tree-delete-form { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); margin: 0; }
|
||||
.plugin-tree .tree-delete-btn { font-size: .9rem; line-height: 1; padding: .15rem; color: #dc3545; background: transparent; border: 0; }
|
||||
.plugin-tree .tree-delete-btn:hover { color: #b02a37; }
|
||||
.plugin-tree .tree-move-btn { position: absolute; right: 1.75rem; top: 50%; transform: translateY(-50%); font-size: .9rem; line-height: 1; padding: .15rem; color: #6c757d; text-decoration: none; }
|
||||
.plugin-tree .tree-move-btn:hover { color: #0d6efd; }
|
||||
.plugin-tree li > .tree-link { padding-right: 3.5rem; }
|
||||
.plugin-file-tree .card-body { max-height: 70vh; overflow-y: auto; }
|
||||
@media (max-width: 768px) {
|
||||
.plugin-editor-layout { flex-direction: column; }
|
||||
.plugin-file-tree { width: 100%; flex-basis: auto; max-height: 320px; }
|
||||
.plugin-file-tree .card-body { max-height: 260px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-pencil"></i> {{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }}</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#pluginUploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.plugin_new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-pencil"></i> {{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }}</h2>
|
||||
<a href="/admin/plugins" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Upload form (collapsed by default) #}
|
||||
<div class="collapse mb-3" id="pluginUploadForm">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/plugins-file-upload" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="plugin" value="{{ pluginName }}">
|
||||
<input type="hidden" name="dir" value="" id="pluginUploadDir">
|
||||
<div class="mb-3">
|
||||
<label for="pluginUploadFile" class="form-label">{{ ta.select_files|default('Bestanden selecteren') }}</label>
|
||||
<input type="file" class="form-control" id="pluginUploadFile" 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,.css,.scss,.js,.json,.html,.md">
|
||||
<small class="form-text text-muted">{{ ta.plugin_upload_help|default('Bestanden worden geüpload naar assets/ van deze plugin. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD.') }}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload_to_folder|default('Uploaden') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-editor-layout">
|
||||
{# File tree sidebar #}
|
||||
<div class="plugin-file-tree">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted fw-semibold"><i class="bi bi-folder2-open"></i> {{ ta.plugin_files|default('Plugin bestanden') }}</span>
|
||||
</div>
|
||||
<div class="card-body p-2" id="pluginFileTree">
|
||||
{% if files is empty %}
|
||||
<div class="small text-muted p-2">{{ ta.plugin_no_files|default('Geen bestanden gevonden.') }}</div>
|
||||
{% else %}
|
||||
{{ tree_macros.tree(files, pluginName, relFile, editableExts, csrf_token) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Editor main panel #}
|
||||
<div class="plugin-editor-main">
|
||||
{% if relFile %}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><i class="bi bi-folder2-open"></i> {{ pluginName }}</li>
|
||||
{% set crumbPath = '' %}
|
||||
{% set parts = relFile|split('/') %}
|
||||
{% for part in parts %}
|
||||
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ part : part %}
|
||||
{% if loop.last %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item"><a href="/admin/plugins-edit?plugin={{ pluginName|url_encode }}&file={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if isEditable %}
|
||||
<form method="POST" action="/admin/plugins-edit?plugin={{ pluginName|url_encode }}&file={{ relFile|url_encode }}" id="editor-form">
|
||||
<form method="POST" action="/admin/plugins-edit?plugin={{ pluginName|url_encode }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="editor-toolbar" id="editor-toolbar"></div>
|
||||
<div class="editor-wrapper">
|
||||
<textarea name="content" id="editor-textarea" data-ext="{{ fileExt }}">{{ fileContent }}</textarea>
|
||||
<textarea name="content" id="editor-textarea" data-ext="php">{{ pluginContent }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,47 +32,6 @@
|
||||
<a href="/admin/plugins" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<i class="bi bi-file-earmark-slash display-6 d-block mb-2"></i>
|
||||
{% if relFile %}
|
||||
{{ ta.plugin_not_editable|default('Dit bestandstype kan niet in de editor bewerkt worden.') }}
|
||||
{% else %}
|
||||
{{ ta.plugin_select_file|default('Selecteer een bestand uit de zijbalk om te bewerken.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# New file modal #}
|
||||
<div class="modal fade" id="newFileModal" tabindex="-1" aria-labelledby="newFileModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/plugins-edit?plugin={{ pluginName|url_encode }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="new_file">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.plugin_new_file_title|default('Nieuw bestand aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="newFilenameInput" class="form-label">{{ ta.plugin_file_path|default('Bestandspad binnen plugin') }}</label>
|
||||
<input type="text" class="form-control" id="newFilenameInput" name="new_filename" placeholder="Bijv. helper.php of assets/css/extra.css" required autofocus>
|
||||
<div class="form-text">{{ ta.plugin_file_path_help|default('Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-arrows-move"></i> {{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }}</h2>
|
||||
<a href="/admin/plugins-edit?plugin={{ pluginName|url_encode }}&file={{ relFile|url_encode }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/plugins-file-move?plugin={{ pluginName|url_encode }}&file={{ relFile|url_encode }}" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="plugin" value="{{ pluginName }}">
|
||||
<input type="hidden" name="file" value="{{ relFile }}">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
{{ ta.move_prefix|default('Verplaats') }} <strong>{{ itemName }}</strong>
|
||||
{% if itemDir %}<span class="text-muted">({{ pluginName }}/{{ itemDir }})</span>{% else %}<span class="text-muted">({{ pluginName }}/)</span>{% endif %}
|
||||
{{ ta.move_to|default('naar:') }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="destination" class="form-label">{{ ta.target_folder|default('Doelmap') }} <small class="text-muted">({{ pluginName }}/)</small></label>
|
||||
<select class="form-select" id="destination" name="destination" required>
|
||||
{% for directory in directories %}
|
||||
<option value="{{ directory }}">{{ directory == '' ? '(plugin root)' : directory }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.move|default('Verplaatsen') }}
|
||||
</button>
|
||||
<a href="/admin/plugins-edit?plugin={{ pluginName|url_encode }}&file={{ relFile|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -13,21 +13,6 @@
|
||||
<input type="text" class="form-control" id="name" name="name" required autofocus>
|
||||
<small class="form-text text-muted">{{ ta.plugin_name_help|default('Alleen letters, cijfers, underscores en streepjes.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plugin type</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="type" id="type-content" value="content" checked>
|
||||
<label class="form-check-label" for="type-content">
|
||||
<strong>Content plugin</strong> — Verschijnt in de sidebar op content pagina's
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="type" id="type-system" value="system">
|
||||
<label class="form-check-label" for="type-system">
|
||||
<strong>Systeem plugin</strong> — Voegt functionaliteit toe aan het CMS (admin menu, API)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@@ -38,31 +38,31 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<div class="btn-group w-100" role="group" aria-label="{{ ta.plugin_actions|default('Plugin acties') }}">
|
||||
<div class="btn-group w-100" role="group">
|
||||
{% if plugin.protected or plugin.essential %}
|
||||
<span class="btn btn-sm btn-outline-secondary disabled" title="{{ ta.essential_title|default('Essentiële plugin - kan niet worden bewerkt, gedeactiveerd of verwijderd') }}" aria-label="{{ ta.essential|default('Essentieel') }}">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
<span class="btn btn-sm btn-outline-secondary disabled" title="{{ ta.essential_title|default('Essentiële plugin - kan niet worden bewerkt, gedeactiveerd of verwijderd') }}">
|
||||
<i class="bi bi-shield-check"></i> {{ ta.essential|default('Essentieel') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="/admin/plugins-edit?plugin={{ plugin.name|url_encode }}" class="btn btn-sm btn-outline-primary" title="{{ ta.edit|default('Bewerken') }}" aria-label="{{ ta.edit|default('Bewerken') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
<a href="/admin/plugins-edit?plugin={{ plugin.name|url_encode }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> {{ ta.edit|default('Bewerken') }}
|
||||
</a>
|
||||
{% if plugin.hasConfig %}
|
||||
<a href="/admin/plugins-config?plugin={{ plugin.name|url_encode }}" class="btn btn-sm btn-outline-info" title="{{ ta.config|default('Configuratie') }}" aria-label="{{ ta.config|default('Configuratie') }}">
|
||||
<i class="bi bi-gear"></i>
|
||||
<a href="/admin/plugins-config?plugin={{ plugin.name|url_encode }}" class="btn btn-sm btn-outline-info">
|
||||
<i class="bi bi-gear"></i> {{ ta.config|default('Config') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<form method="POST" action="/admin/plugins-toggle" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="plugin" value="{{ plugin.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-{{ plugin.enabled ? 'outline-warning' : 'outline-success' }}" title="{{ plugin.enabled ? ta.deactivate|default('Deactiveren') : ta.activate|default('Activeren') }}" aria-label="{{ plugin.enabled ? ta.deactivate|default('Deactiveren') : ta.activate|default('Activeren') }}">
|
||||
<i class="bi bi-{{ plugin.enabled ? 'pause' : 'play' }}"></i>
|
||||
<button type="submit" class="btn btn-sm btn-{{ plugin.enabled ? 'outline-warning' : 'outline-success' }}">
|
||||
<i class="bi bi-{{ plugin.enabled ? 'pause' : 'play' }}"></i> {{ plugin.enabled ? ta.deactivate|default('Deactiveren') : ta.activate|default('Activeren') }}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/admin/plugins-delete" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_plugin|default('Weet je zeker dat je deze plugin wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="plugin" value="{{ plugin.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ ta.edit_profile|default('Profiel bewerken') }}: {{ target_user.username }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-person-gear"></i> {{ ta.edit_profile|default('Profiel bewerken') }}</h2>
|
||||
<a href="/admin/users" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back_to_users|default('Terug naar gebruikers') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-person-circle"></i> {{ ta.profile_info|default('Profiel gegevens') }}
|
||||
{% if is_self %}
|
||||
<span class="badge bg-info ms-2">{{ ta.you|default('Jij') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="profile">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
||||
<input type="text" class="form-control" value="{{ target_user.username }}" disabled>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="profile_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="profile_email" name="profile_email" value="{{ target_user.email|default('') }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="profile_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
||||
<input type="text" class="form-control" id="profile_author_name" name="profile_author_name" value="{{ target_user.author_name|default('') }}">
|
||||
<small class="form-text text-muted">{{ ta.author_name_help|default('Getoond als auteur op de website.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="profile_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="profile_author_email" name="profile_author_email" value="{{ target_user.author_email|default('') }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if can_change_password is not defined or can_change_password %}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-key"></i> {{ ta.change_password|default('Wachtwoord wijzigen') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}" onsubmit="return validatePassword();">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">{{ ta.new_password|default('Nieuw wachtwoord') }}</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="8">
|
||||
<small class="form-text text-muted">{{ ta.password_help|default('Minimaal 8 tekens.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">{{ ta.confirm_password|default('Bevestig wachtwoord') }}</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required minlength="8">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-key"></i> {{ ta.change_password|default('Wachtwoord wijzigen') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card shadow-sm mb-4 border-warning">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<i class="bi bi-key"></i> {{ ta.change_password|default('Wachtwoord wijzigen') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><i class="bi bi-info-circle"></i> {{ ta.cannot_change_password_role|default('Wachtwoord wijzigen is niet toegestaan in deze rol.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-shield-lock"></i> {{ ta.col_role|default('Rol') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2"><strong>{{ ta.current_role|default('Huidige rol') }}</strong></p>
|
||||
<p>
|
||||
<span class="badge bg-{{ target_user.role == 'admin' ? 'danger' : (target_user.role == 'content-manager' ? 'primary' : (target_user.role == 'bi-manager' ? 'success' : 'warning')) }}">
|
||||
{{ target_user.role_label|default(target_user.role) }}
|
||||
</span>
|
||||
</p>
|
||||
{% if is_self %}
|
||||
<div class="alert alert-info mb-0" role="alert">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
{{ ta.cannot_change_own_role|default('Je kunt je eigen rol niet wijzigen.') }}
|
||||
</div>
|
||||
{% else %}
|
||||
<hr>
|
||||
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="change_role">
|
||||
<div class="mb-3">
|
||||
<label for="new_role" class="form-label">{{ ta.new_role|default('Nieuwe rol') }}</label>
|
||||
<select class="form-select" id="new_role" name="new_role">
|
||||
{% for roleKey, roleLabel in roles %}
|
||||
<option value="{{ roleKey }}" {{ target_user.role == roleKey ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary w-100">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.change|default('Wijzigen') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not is_self %}
|
||||
<div class="card shadow-sm border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<i class="bi bi-exclamation-triangle"></i> {{ ta.danger_zone|default('Gevarenzone') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/users-edit?user={{ target_user.username }}" onsubmit="return confirm('{{ ta.confirm_delete_user|default('Weet je zeker dat je deze gebruiker wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<button type="submit" class="btn btn-danger w-100">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete_user|default('Gebruiker verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function validatePassword() {
|
||||
var p1 = document.getElementById('new_password').value;
|
||||
var p2 = document.getElementById('confirm_password').value;
|
||||
if (p1 !== p2) {
|
||||
alert('{{ ta.passwords_no_match|default('Wachtwoorden komen niet overeen.') }}');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,65 +0,0 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ ta.new_user|default('Nieuwe gebruiker') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-plus-circle"></i> {{ ta.new_user|default('Nieuwe gebruiker') }}</h2>
|
||||
<a href="/admin/users" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back_to_users|default('Terug naar gebruikers') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-person-plus"></i> {{ ta.add_user|default('Gebruiker toevoegen') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/users-new">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<div class="mb-3">
|
||||
<label for="new_username" class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
||||
<input type="text" class="form-control" id="new_username" name="new_username" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">{{ ta.password|default('Wachtwoord') }}</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="8">
|
||||
<small class="form-text text-muted">{{ ta.password_help|default('Minimaal 8 tekens.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="new_email" name="new_email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
||||
<input type="text" class="form-control" id="new_author_name" name="new_author_name">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="new_author_email" name="new_author_email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_role" class="form-label">{{ ta.col_role|default('Rol') }}</label>
|
||||
<select class="form-select" id="new_role" name="new_role">
|
||||
{% for roleKey, roleLabel in roles %}
|
||||
<option value="{{ roleKey }}" {{ roleKey == 'content-manager' ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.add_user|default('Gebruiker toevoegen') }}
|
||||
</button>
|
||||
<a href="/admin/users" class="btn btn-outline-secondary">
|
||||
{{ ta.cancel|default('Annuleren') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,45 +5,18 @@
|
||||
{% block content %}
|
||||
<h2 class="mb-4"><i class="bi bi-people"></i> {{ ta.users|default('Gebruikers') }}</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-list"></i> {{ ta.users_list|default('Gebruikerslijst') }}
|
||||
<div class="card-header">
|
||||
<i class="bi bi-list"></i> {{ ta.users|default('Gebruikers') }}
|
||||
</div>
|
||||
<a href="/admin/users-new" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> {{ ta.new_user|default('Nieuwe gebruiker') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/admin/users" class="row g-2 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control" name="search" value="{{ search|default('') }}" placeholder="{{ ta.search_users|default('Zoek op gebruikersnaam, e-mail of auteur naam...') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select class="form-select" name="role">
|
||||
<option value="">{{ ta.all_roles|default('Alle rollen') }}</option>
|
||||
{% for roleKey, roleLabel in roles %}
|
||||
<option value="{{ roleKey }}" {{ role_filter == roleKey ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-outline-primary w-100">
|
||||
<i class="bi bi-funnel"></i> {{ ta.filter|default('Filteren') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ ta.username|default('Gebruikersnaam') }}</th>
|
||||
<th>{{ ta.col_role|default('Rol') }}</th>
|
||||
<th>{{ ta.login_email|default('Login e-mail') }}</th>
|
||||
<th>{{ ta.col_created|default('Aangemaakt') }}</th>
|
||||
<th>{{ ta.col_actions|default('Acties') }}</th>
|
||||
</tr>
|
||||
@@ -53,7 +26,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<i class="bi bi-person-circle"></i>
|
||||
<a href="/admin/users-edit?user={{ username }}">{{ username }}</a>
|
||||
{{ username }}
|
||||
{% if username == user.username %}
|
||||
<span class="badge bg-info">{{ ta.you|default('Jij') }}</span>
|
||||
{% endif %}
|
||||
@@ -63,27 +36,65 @@
|
||||
{{ data.role_label|default(data.role) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted">{{ data.email|default('-') }}</td>
|
||||
<td class="text-muted">{{ data.created|default(ta.unknown|default('Onbekend')) }}</td>
|
||||
<td>
|
||||
<a href="/admin/users-edit?user={{ username }}" class="btn btn-sm btn-outline-primary" title="{{ ta.edit_profile|default('Profiel bewerken') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% if username != user.username %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#roleModal-{{ username }}">
|
||||
<i class="bi bi-person-gear"></i> {{ ta.col_role|default('Rol') }}
|
||||
</button>
|
||||
<form method="POST" action="/admin/users" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_user|default('Weet je zeker dat je deze gebruiker wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="delete_username" value="{{ username }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Role change modal -->
|
||||
<div class="modal fade" id="roleModal-{{ username }}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/users">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="change_role">
|
||||
<input type="hidden" name="role_username" value="{{ username }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-person-gear"></i> {{ ta.change_role|default('Rol wijzigen: ') }}{{ username }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ ta.current_role|default('Huidige rol') }}</label>
|
||||
<p><span class="badge bg-secondary">{{ data.role_label|default(data.role) }}</span></p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_role-{{ username }}" class="form-label">{{ ta.new_role|default('Nieuwe rol') }}</label>
|
||||
<select class="form-select" id="new_role-{{ username }}" name="new_role">
|
||||
{% for roleKey, roleLabel in roles %}
|
||||
<option value="{{ roleKey }}" {{ data.role == roleKey ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.change|default('Wijzigen') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#profileModal">
|
||||
<i class="bi bi-pencil"></i> {{ ta.edit_profile|default('Profiel bewerken') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-muted text-center py-4">{{ ta.no_users|default('Geen gebruikers gevonden.') }}</td>
|
||||
<td colspan="4" class="text-muted text-center py-4">{{ ta.no_users|default('Geen gebruikers gevonden.') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -91,4 +102,86 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<!-- Profile edit card for current user -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-person-circle"></i> {{ ta.my_profile|default('Mijn profiel') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/users">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="profile">
|
||||
<input type="hidden" name="profile_username" value="{{ user.username }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
||||
<input type="text" class="form-control" value="{{ user.username }}" disabled>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="profile_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="profile_email" name="profile_email" value="{{ user.email|default('') }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="profile_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
||||
<input type="text" class="form-control" id="profile_author_name" name="profile_author_name" value="{{ user.author_name|default('') }}">
|
||||
<small class="form-text text-muted">{{ ta.author_name_help|default('Getoond als auteur op de website.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="profile_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="profile_author_email" name="profile_author_email" value="{{ user.author_email|default('') }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New user card -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle"></i> {{ ta.new_user|default('Nieuwe gebruiker') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/users">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<div class="mb-3">
|
||||
<label for="new_username" class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
|
||||
<input type="text" class="form-control" id="new_username" name="new_username" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">{{ ta.password|default('Wachtwoord') }}</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" required>
|
||||
<small class="form-text text-muted">{{ ta.password_help|default('Minimaal 8 tekens.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="new_email" name="new_email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
|
||||
<input type="text" class="form-control" id="new_author_name" name="new_author_name">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||
<input type="email" class="form-control" id="new_author_email" name="new_author_email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_role" class="form-label">{{ ta.col_role|default('Rol') }}</label>
|
||||
<select class="form-select" id="new_role" name="new_role">
|
||||
{% for roleKey, roleLabel in roles %}
|
||||
<option value="{{ roleKey }}" {{ roleKey == 'content-manager' ? 'selected' : '' }}>{{ roleLabel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.add_user|default('Gebruiker toevoegen') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* CodePress CMS — Admin wachtwoord reset CLI
|
||||
*
|
||||
* Gebruik:
|
||||
* php cli/reset-admin-password.php [gebruikersnaam] [nieuw_wachtwoord]
|
||||
*
|
||||
* Als geen wachtwoord opgegeven, wordt een random wachtwoord gegenereerd.
|
||||
* Wisst ook brute-force lockout voor de gebruiker.
|
||||
*
|
||||
* Voorbeelden:
|
||||
* php cli/reset-admin-password.php admin
|
||||
* php cli/reset-admin-password.php admin MijnNieuweWachtwoord123
|
||||
*/
|
||||
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
fwrite(STDERR, "Dit script kan alleen via de CLI uitgevoerd worden.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$rootDir = dirname(__DIR__);
|
||||
require_once $rootDir . '/admin/src/AdminAuth.php';
|
||||
|
||||
$appConfig = require $rootDir . '/admin/config/app.php';
|
||||
|
||||
// Argumenten parsen
|
||||
$username = $argv[1] ?? '';
|
||||
$password = $argv[2] ?? '';
|
||||
|
||||
if ($username === '') {
|
||||
echo "CodePress CMS — Admin wachtwoord reset\n";
|
||||
echo "========================================\n\n";
|
||||
echo "Gebruik: php cli/reset-admin-password.php [gebruikersnaam] [nieuw_wachtwoord]\n\n";
|
||||
echo "Als geen wachtwoord opgegeven, wordt een random wachtwoord gegenereerd.\n\n";
|
||||
echo "Voorbeelden:\n";
|
||||
echo " php cli/reset-admin-password.php admin\n";
|
||||
echo " php cli/reset-admin-password.php admin MijnNieuweWachtwoord123\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// AdminAuth aanmaken (zonder sessie requirements)
|
||||
$auth = new AdminAuth($appConfig);
|
||||
|
||||
// Controleren of de gebruiker bestaat
|
||||
$users = $auth->getUsers();
|
||||
if (!isset($users[$username])) {
|
||||
fwrite(STDERR, "Fout: Gebruiker '$username' niet gevonden.\n");
|
||||
fwrite(STDERR, "Beschikbare gebruikers: " . implode(', ', array_keys($users)) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Wachtwoord genereren als niet opgegeven
|
||||
if ($password === '') {
|
||||
$password = bin2hex(random_bytes(12));
|
||||
$generated = true;
|
||||
} else {
|
||||
$generated = false;
|
||||
}
|
||||
|
||||
// Wachtwoord wijzigen
|
||||
$result = $auth->changePassword($username, $password);
|
||||
if (!$result['success']) {
|
||||
fwrite(STDERR, "Fout: " . $result['message'] . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Lockout wissen
|
||||
$auth->clearLockout($username);
|
||||
|
||||
echo "CodePress CMS — Admin wachtwoord reset\n";
|
||||
echo "========================================\n\n";
|
||||
echo "Gebruiker: $username\n";
|
||||
echo "Wachtwoord: $password\n";
|
||||
if ($generated) {
|
||||
echo " (automatisch gegenereerd — bewaar dit veilig)\n";
|
||||
}
|
||||
echo "\n";
|
||||
echo "Lockout voor '$username' is gewist.\n";
|
||||
echo "Je kunt nu inloggen via /admin met het nieuwe wachtwoord.\n";
|
||||
@@ -3,7 +3,7 @@
|
||||
# WCAG 2.1 AA Accessibility Test Suite for CodePress CMS
|
||||
# Tests for web accessibility compliance
|
||||
|
||||
BASE_URL="http://development.codepress.noorlander.info"
|
||||
BASE_URL="http://localhost:8080"
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# CodePress CMS Functional Test Suite v1.5.0
|
||||
# Tests core functionality, new features, and regressions
|
||||
|
||||
BASE_URL="http://development.codepress.noorlander.info"
|
||||
BASE_URL="http://localhost:8080"
|
||||
TEST_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
@@ -80,11 +80,14 @@ echo ""
|
||||
echo "2. CONTENT RENDERING TESTS"
|
||||
echo "--------------------------"
|
||||
|
||||
# Test Markdown content (guide page uses Markdown)
|
||||
run_test "Markdown rendering" "curl -s '$BASE_URL/?guide' | grep -c '<h1>'" "1"
|
||||
# Test Markdown content
|
||||
run_test "Markdown rendering" "curl -s '$BASE_URL/?page=demo/content-only' | grep -c '<h1>'" "1"
|
||||
|
||||
# Test 404 handling for non-existent pages
|
||||
run_test "404 content handling" "curl -s '$BASE_URL/?page=nonexistent' | grep -c '404'" "1"
|
||||
# Test HTML content
|
||||
run_test "HTML content" "curl -s '$BASE_URL/?page=demo/html-demo' | grep -c '<h1>'" "1"
|
||||
|
||||
# Test PHP content
|
||||
run_test "PHP content" "curl -s '$BASE_URL/?page=demo/php-demo' | grep -c 'PHP Version'" "1"
|
||||
|
||||
echo ""
|
||||
echo "3. NAVIGATION TESTS"
|
||||
@@ -94,7 +97,7 @@ echo "-------------------"
|
||||
run_test "Menu generation" "curl -s '$BASE_URL/' | grep -c 'nav-item'" "2"
|
||||
|
||||
# Test breadcrumb navigation
|
||||
run_test "Breadcrumb navigation" "curl -s '$BASE_URL/?guide' | grep -c 'breadcrumb'" "1"
|
||||
run_test "Breadcrumb navigation" "curl -s '$BASE_URL/?page=demo/content-only' | grep -c 'breadcrumb'" "1"
|
||||
|
||||
echo ""
|
||||
echo "4. TEMPLATE SYSTEM TESTS"
|
||||
@@ -240,6 +243,7 @@ Functional testing performed on CodePress CMS v1.5.0 covering core functionality
|
||||
|
||||
### Plugin System
|
||||
- **HTMLBlock Plugin**: Custom HTML blocks in sidebar
|
||||
- **MQTTTracker Plugin**: Real-time analytics and tracking
|
||||
- **Plugin Manager**: Centralized plugin loading system
|
||||
|
||||
### Enhanced Documentation
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# CodePress CMS Penetration Test Script
|
||||
# WARNING: Only run this on systems you have permission to test!
|
||||
|
||||
TARGET="http://development.codepress.noorlander.info"
|
||||
TARGET="http://localhost:8080"
|
||||
RESULTS_FILE="pentest_results.txt"
|
||||
|
||||
echo "🔒 CodePress CMS Penetration Test" > $RESULTS_FILE
|
||||
|
||||
+11
-113
@@ -48,10 +48,8 @@ class CodePressCMS {
|
||||
|
||||
// Initialize plugin manager (files already loaded in cms/core/index.php)
|
||||
$enabledPlugins = $this->config['enabled_plugins'] ?? [];
|
||||
$siteDefaultLang = $this->config['language']['default'] ?? 'nl';
|
||||
$this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins', $enabledPlugins, $siteDefaultLang);
|
||||
$this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins', $enabledPlugins);
|
||||
$api = new CMSAPI($this);
|
||||
$api->setPluginManager($this->pluginManager);
|
||||
$this->pluginManager->setAPI($api);
|
||||
|
||||
$this->buildMenu();
|
||||
@@ -452,28 +450,9 @@ class CodePressCMS {
|
||||
return $this->getGuidePage();
|
||||
}
|
||||
|
||||
// Determine if the request has a valid language prefix
|
||||
$availableLangs = array_keys($this->getAvailableLanguages());
|
||||
$requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/';
|
||||
$requestPath = ltrim($requestPath, '/');
|
||||
$langFromUrl = '';
|
||||
if ($requestPath !== '' && in_array(explode('/', $requestPath)[0], $availableLangs, true)) {
|
||||
$langFromUrl = explode('/', $requestPath)[0];
|
||||
}
|
||||
|
||||
// No language prefix and not the root → 404
|
||||
if ($langFromUrl === '' && $requestPath !== '' && $requestPath !== 'favicon.ico') {
|
||||
return $this->getError404();
|
||||
}
|
||||
|
||||
// Check if content directory is empty — show welcome page for new installations
|
||||
// Check if content directory is empty
|
||||
if ($this->isContentDirEmpty()) {
|
||||
$requestedPage = $_GET['page'] ?? '';
|
||||
// Only show welcome page on the home URL; everything else is a 404
|
||||
if (empty($requestedPage) || $requestedPage === $this->getEffectiveDefaultPage()) {
|
||||
return $this->getWelcomePage();
|
||||
}
|
||||
return $this->getError404();
|
||||
return $this->getGuidePage();
|
||||
}
|
||||
|
||||
$page = $_GET['page'] ?? $this->getEffectiveDefaultPage();
|
||||
@@ -1069,60 +1048,9 @@ class CodePressCMS {
|
||||
$files = scandir($contentDir);
|
||||
$files = array_diff($files, ['.', '..']);
|
||||
|
||||
// Filter out hidden files (e.g. .gitkeep) — only count visible content
|
||||
$files = array_filter($files, function($f) {
|
||||
return $f[0] !== '.';
|
||||
});
|
||||
|
||||
return empty($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get welcome page content for new installations (empty content directory)
|
||||
*
|
||||
* Shows a clear "new installation" message with next steps when the
|
||||
* content directory has no content files yet.
|
||||
*
|
||||
* @return array Welcome page data
|
||||
*/
|
||||
private function getWelcomePage() {
|
||||
$adminUrl = $this->buildAdminUrl();
|
||||
$guideUrl = '/' . $this->currentLanguage . '/guide';
|
||||
|
||||
$content = '<div class="welcome-page">';
|
||||
$content .= '<h1>' . htmlspecialchars($this->t('welcome_title')) . '</h1>';
|
||||
$content .= '<p class="alert alert-info" role="alert">';
|
||||
$content .= '<i class="bi bi-info-circle me-2" aria-hidden="true"></i>';
|
||||
$content .= htmlspecialchars($this->t('welcome_intro'));
|
||||
$content .= '</p>';
|
||||
|
||||
$content .= '<h2>' . htmlspecialchars($this->t('welcome_next_steps')) . '</h2>';
|
||||
$content .= '<ol class="list-group list-group-numbered mb-4">';
|
||||
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_1')) . '</li>';
|
||||
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_2')) . '</li>';
|
||||
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_3')) . '</li>';
|
||||
$content .= '</ol>';
|
||||
|
||||
$content .= '<div class="d-flex flex-column flex-md-row gap-2 mb-4">';
|
||||
$content .= '<a href="' . htmlspecialchars($adminUrl) . '" class="btn btn-primary">';
|
||||
$content .= '<i class="bi bi-gear me-1" aria-hidden="true"></i> ';
|
||||
$content .= htmlspecialchars($this->t('welcome_admin_link'));
|
||||
$content .= '</a>';
|
||||
$content .= '<a href="' . htmlspecialchars($guideUrl) . '" class="btn btn-outline-secondary">';
|
||||
$content .= '<i class="bi bi-book me-1" aria-hidden="true"></i> ';
|
||||
$content .= htmlspecialchars($this->t('welcome_guide_link'));
|
||||
$content .= '</a>';
|
||||
$content .= '</div>';
|
||||
$content .= '</div>';
|
||||
|
||||
return [
|
||||
'title' => $this->t('welcome_title'),
|
||||
'content' => $content,
|
||||
'layout' => 'full_content',
|
||||
'metadata' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get guide page content based on user language
|
||||
*
|
||||
@@ -1275,22 +1203,9 @@ class CodePressCMS {
|
||||
$content .= '<p>' . $this->t('directory_empty') . '.</p>';
|
||||
}
|
||||
|
||||
// Check for index.md in this directory for layout/plugins metadata
|
||||
$indexFile = $dirPath . '/index.md';
|
||||
$layout = 'full_content';
|
||||
$metadata = [];
|
||||
if (file_exists($indexFile)) {
|
||||
$indexContent = file_get_contents($indexFile);
|
||||
$parsed = $this->parseMetadata($indexContent);
|
||||
$metadata = $parsed['metadata'];
|
||||
$layout = $metadata['layout'] ?? 'full_content';
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'layout' => $layout,
|
||||
'metadata' => $metadata,
|
||||
'content' => $content
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1300,17 +1215,9 @@ class CodePressCMS {
|
||||
* @return array 404 page data
|
||||
*/
|
||||
private function getError404() {
|
||||
$staticFile = __DIR__ . '/../../../admin/static/404.html';
|
||||
$body = file_exists($staticFile)
|
||||
? file_get_contents($staticFile)
|
||||
: '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>';
|
||||
|
||||
return [
|
||||
'title' => $this->t('page_not_found'),
|
||||
'content' => $body,
|
||||
'layout' => 'full_content',
|
||||
'metadata' => [],
|
||||
'is_404' => true,
|
||||
'content' => '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1333,11 +1240,6 @@ class CodePressCMS {
|
||||
$this->pluginManager->doAction('onPageLoad', $page);
|
||||
$this->pluginManager->doAction('onBeforeRender');
|
||||
|
||||
// Set 404 status for not-found pages (rendered through the theme)
|
||||
if (!empty($page['is_404'])) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$menu = $this->getMenu();
|
||||
|
||||
// Get homepage title
|
||||
@@ -1380,10 +1282,6 @@ class CodePressCMS {
|
||||
'author_name' => $this->config['author']['name'] ?? 'CodePress Developer',
|
||||
'author_website' => $this->normalizeUrl($this->config['author']['website'] ?? '') ?: '#',
|
||||
'author_git' => $this->config['author']['git'] ?? '',
|
||||
// Per-page author metadata (from frontmatter, if present)
|
||||
'page_author_name' => htmlspecialchars($page['metadata']['author_name'] ?? ''),
|
||||
'page_author_email' => htmlspecialchars($page['metadata']['author_email'] ?? ''),
|
||||
'page_created' => htmlspecialchars($page['metadata']['created'] ?? ''),
|
||||
'seo_description' => $this->config['seo']['description'] ?? 'CodePress CMS - Lightweight file-based content management system',
|
||||
'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based',
|
||||
'block_ai_bots' => !empty($this->config['security']['block_ai_bots']),
|
||||
@@ -1591,7 +1489,7 @@ class CodePressCMS {
|
||||
$isExpanded = $this->folderContainsActivePage($item['children']);
|
||||
|
||||
if ($level === 0) {
|
||||
// Root level folders - Bootstrap dropdown
|
||||
// Root level folders
|
||||
$html .= '<li class="nav-item dropdown">';
|
||||
$html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
|
||||
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
|
||||
@@ -1601,12 +1499,12 @@ class CodePressCMS {
|
||||
$html .= '</ul>';
|
||||
$html .= '</li>';
|
||||
} else {
|
||||
// Nested folders - CSS hover submenu (unlimited depth)
|
||||
$html .= '<li class="dropdown-submenu' . ($isExpanded ? ' show' : '') . '">';
|
||||
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '" role="button" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
|
||||
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-right"></i>';
|
||||
// Nested folders in dropdown
|
||||
$html .= '<li class="dropdown-submenu">';
|
||||
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">';
|
||||
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
|
||||
$html .= '</a>';
|
||||
$html .= '<ul class="dropdown-menu' . ($isExpanded ? ' show' : '') . '" aria-labelledby="' . $folderId . '">';
|
||||
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
|
||||
$html .= $this->renderMenu($item['children'], $level + 1);
|
||||
$html .= '</ul>';
|
||||
$html .= '</li>';
|
||||
|
||||
@@ -241,21 +241,4 @@ class ContentAPI
|
||||
{
|
||||
return isset($_GET['search']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the author metadata for the current page.
|
||||
* Returns author_name, author_email and created from the page frontmatter.
|
||||
*
|
||||
* @return array Author metadata with keys: author_name, author_email, created
|
||||
*/
|
||||
public function getPageAuthor(): array
|
||||
{
|
||||
$page = $this->cms->getPage();
|
||||
$metadata = $page['metadata'] ?? [];
|
||||
return [
|
||||
'author_name' => $metadata['author_name'] ?? '',
|
||||
'author_email' => $metadata['author_email'] ?? '',
|
||||
'created' => $metadata['created'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,22 +169,8 @@ class ThemeManager {
|
||||
$compiler = new Compiler();
|
||||
$compiler->setImportPaths($this->themeDir . '/assets/scss');
|
||||
$css = $compiler->compileString(file_get_contents($scssFile))->getCss();
|
||||
|
||||
// Remove read-only before writing (file may be locked from previous compile)
|
||||
if (is_file($outFile)) {
|
||||
@chmod($outFile, 0644);
|
||||
}
|
||||
if (is_file($cacheFile)) {
|
||||
@chmod($cacheFile, 0644);
|
||||
}
|
||||
|
||||
file_put_contents($outFile, $css);
|
||||
file_put_contents($cacheFile, (string)$mtime);
|
||||
|
||||
// Set read-only to prevent manual edits
|
||||
@chmod($outFile, 0444);
|
||||
@chmod($cacheFile, 0444);
|
||||
|
||||
return $outFile;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
|
||||
@@ -194,11 +180,16 @@ class ThemeManager {
|
||||
|
||||
/**
|
||||
* Get the public URL for the theme CSS.
|
||||
* Uses assets/css_compiled/theme.css (compiled from SCSS by scssphp).
|
||||
* Priority: 1) assets/css/theme.css (manual), 2) assets/css_compiled/theme.css (compiled), 3) null
|
||||
*/
|
||||
public function getCssUrl(): ?string {
|
||||
$manualCss = $this->themeDir . '/assets/css/theme.css';
|
||||
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
|
||||
|
||||
if (is_file($manualCss)) {
|
||||
return $this->getThemeAssetUrl('css/theme.css');
|
||||
}
|
||||
|
||||
if (is_file($compiledCss)) {
|
||||
return $this->getThemeAssetUrl('css_compiled/theme.css');
|
||||
}
|
||||
|
||||
@@ -8,24 +8,11 @@ class AdminPluginAPI implements PluginAPIInterface
|
||||
{
|
||||
private array $config;
|
||||
private string $projectRoot;
|
||||
private string $adminLanguage;
|
||||
private ?PluginManager $pluginManager = null;
|
||||
|
||||
public function __construct(array $siteConfig, string $projectRoot = '')
|
||||
{
|
||||
$this->config = $siteConfig;
|
||||
$this->projectRoot = $projectRoot !== '' ? $projectRoot : dirname(__DIR__, 3);
|
||||
$this->adminLanguage = $siteConfig['admin_language']
|
||||
?? ($siteConfig['language']['default'] ?? 'nl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the admin PluginManager so plugins can resolve their own
|
||||
* translations through the same fallback chain.
|
||||
*/
|
||||
public function setPluginManager(PluginManager $pm): void
|
||||
{
|
||||
$this->pluginManager = $pm;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,46 +79,4 @@ class AdminPluginAPI implements PluginAPIInterface
|
||||
}
|
||||
return ['version' => '0.0.0'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active admin language code (e.g. 'nl', 'en').
|
||||
* System plugins should use this to resolve their translations.
|
||||
*/
|
||||
public function getAdminLanguage(): string
|
||||
{
|
||||
return $this->adminLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translations for a plugin in the admin context.
|
||||
*
|
||||
* Fallback chain (handled by PluginManager):
|
||||
* requested language -> plugin default_language -> empty array.
|
||||
*
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @param string|null $lang Override language; defaults to the active admin language
|
||||
* @return array Translations [key => value]
|
||||
*/
|
||||
public function getPluginTranslations(string $pluginName, ?string $lang = null): array
|
||||
{
|
||||
if ($this->pluginManager === null) {
|
||||
return [];
|
||||
}
|
||||
$lang = $lang ?? $this->adminLanguage;
|
||||
return $this->pluginManager->getPluginTranslations($pluginName, $lang, 'admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a single key for a plugin in the admin context.
|
||||
*
|
||||
* @param string $key Translation key
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @param string|null $lang Override language; defaults to the active admin language
|
||||
* @return string Translated string, or $key if not found
|
||||
*/
|
||||
public function t(string $key, string $pluginName, ?string $lang = null): string
|
||||
{
|
||||
$t = $this->getPluginTranslations($pluginName, $lang);
|
||||
return $t[$key] ?? $key;
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,12 @@
|
||||
class CMSAPI implements PluginAPIInterface
|
||||
{
|
||||
private CodePressCMS $cms;
|
||||
private ?PluginManager $pluginManager = null;
|
||||
|
||||
public function __construct(CodePressCMS $cms)
|
||||
{
|
||||
$this->cms = $cms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the front-end PluginManager so content plugins can resolve
|
||||
* their own translations through the same fallback chain.
|
||||
*/
|
||||
public function setPluginManager(PluginManager $pm): void
|
||||
{
|
||||
$this->pluginManager = $pm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current page information
|
||||
*/
|
||||
@@ -230,55 +220,4 @@ class CMSAPI implements PluginAPIInterface
|
||||
{
|
||||
return $this->cms->getAllPageTitles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the author metadata for the current page.
|
||||
* Returns author_name, author_email and created from the page frontmatter.
|
||||
*
|
||||
* @return array Author metadata with keys: author_name, author_email, created
|
||||
*/
|
||||
public function getPageAuthor(): array
|
||||
{
|
||||
$page = $this->cms->getPage();
|
||||
$metadata = $page['metadata'] ?? [];
|
||||
return [
|
||||
'author_name' => $metadata['author_name'] ?? '',
|
||||
'author_email' => $metadata['author_email'] ?? '',
|
||||
'created' => $metadata['created'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translations for a plugin in the front-end context.
|
||||
*
|
||||
* Content plugins follow the current content language. Fallback chain
|
||||
* (handled by PluginManager): requested language -> plugin
|
||||
* default_language -> empty array.
|
||||
*
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @param string|null $lang Override language; defaults to the current content language
|
||||
* @return array Translations [key => value]
|
||||
*/
|
||||
public function getPluginTranslations(string $pluginName, ?string $lang = null): array
|
||||
{
|
||||
if ($this->pluginManager === null) {
|
||||
return [];
|
||||
}
|
||||
$lang = $lang ?? $this->cms->currentLanguage;
|
||||
return $this->pluginManager->getPluginTranslations($pluginName, $lang, 'site');
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a single key for a plugin in the front-end context.
|
||||
*
|
||||
* @param string $key Translation key
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @param string|null $lang Override language; defaults to the current content language
|
||||
* @return string Translated string, or $key if not found
|
||||
*/
|
||||
public function t(string $key, string $pluginName, ?string $lang = null): string
|
||||
{
|
||||
$t = $this->getPluginTranslations($pluginName, $lang);
|
||||
return $t[$key] ?? $key;
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,4 @@
|
||||
interface PluginAPIInterface
|
||||
{
|
||||
public function getConfig(string $key, $default = null);
|
||||
|
||||
/**
|
||||
* Get the translations for a plugin in the current context.
|
||||
*
|
||||
* System plugins resolve against the admin language; content plugins
|
||||
* against the current content language. The implementation handles the
|
||||
* fallback to the plugin's default_language.
|
||||
*
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @param string|null $lang Override language (optional)
|
||||
* @return array Translations [key => value]
|
||||
*/
|
||||
public function getPluginTranslations(string $pluginName, ?string $lang = null): array;
|
||||
|
||||
/**
|
||||
* Translate a single key for a plugin in the current context.
|
||||
*
|
||||
* @param string $key Translation key
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @param string|null $lang Override language (optional)
|
||||
* @return string Translated string, or $key if not found
|
||||
*/
|
||||
public function t(string $key, string $pluginName, ?string $lang = null): string;
|
||||
}
|
||||
@@ -8,25 +8,14 @@ class PluginManager
|
||||
private array $enabledPlugins = [];
|
||||
private array $actions = [];
|
||||
private array $filters = [];
|
||||
private string $siteDefaultLanguage = 'nl';
|
||||
|
||||
public function __construct(string $pluginsPath, array $enabledPlugins = [], string $siteDefaultLanguage = 'nl')
|
||||
public function __construct(string $pluginsPath, array $enabledPlugins = [])
|
||||
{
|
||||
$this->pluginsPath = $pluginsPath;
|
||||
$this->enabledPlugins = $enabledPlugins;
|
||||
$this->siteDefaultLanguage = $siteDefaultLanguage !== '' ? $siteDefaultLanguage : 'nl';
|
||||
$this->loadPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the CMS site default language. Used as a fallback when a plugin
|
||||
* does not declare its own `default_language` in plugin.json.
|
||||
*/
|
||||
public function setSiteDefaultLanguage(string $lang): void
|
||||
{
|
||||
$this->siteDefaultLanguage = $lang !== '' ? $lang : 'nl';
|
||||
}
|
||||
|
||||
public function setAPI($api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
@@ -135,123 +124,6 @@ class PluginManager
|
||||
return in_array($pluginName, $this->enabledPlugins, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the runtime config for a plugin: defaults from plugin.json `settings`
|
||||
* merged with overrides from the plugin's config.json.
|
||||
*
|
||||
* @param string $pluginName Plugin name (directory name)
|
||||
* @return array Resolved config: [key => value, ...]
|
||||
*/
|
||||
public function getPluginConfig(string $pluginName): array
|
||||
{
|
||||
$pluginDir = $this->pluginsPath . '/' . $pluginName;
|
||||
$pluginJsonFile = $pluginDir . '/plugin.json';
|
||||
$configJsonFile = $pluginDir . '/config.json';
|
||||
|
||||
$defaults = [];
|
||||
if (file_exists($pluginJsonFile)) {
|
||||
$pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? [];
|
||||
foreach ($pluginJson['settings'] ?? [] as $setting) {
|
||||
if (isset($setting['key'])) {
|
||||
$defaults[$setting['key']] = $setting['default'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$overrides = [];
|
||||
if (file_exists($configJsonFile)) {
|
||||
$overrides = json_decode(file_get_contents($configJsonFile), true) ?? [];
|
||||
}
|
||||
|
||||
return array_merge($defaults, $overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default (fallback) language declared by a plugin.
|
||||
*
|
||||
* Resolved from (in order):
|
||||
* 1. plugin.json `default_language`
|
||||
* 2. CMS site config `language.default`
|
||||
* 3. 'nl'
|
||||
*
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @return string Language code (e.g. 'nl', 'en')
|
||||
*/
|
||||
public function getPluginDefaultLanguage(string $pluginName): string
|
||||
{
|
||||
$pluginJsonFile = $this->pluginsPath . '/' . $pluginName . '/plugin.json';
|
||||
if (file_exists($pluginJsonFile)) {
|
||||
$pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? [];
|
||||
if (!empty($pluginJson['default_language'])) {
|
||||
return (string)$pluginJson['default_language'];
|
||||
}
|
||||
}
|
||||
return $this->siteDefaultLanguage ?? 'nl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the translations for a plugin for the requested language.
|
||||
*
|
||||
* Fallback chain: requested language -> plugin default language -> empty array.
|
||||
*
|
||||
* @param string $pluginName Plugin directory name
|
||||
* @param string $lang Requested language code
|
||||
* @param string $context Translation context: 'admin' or 'site' (front-end).
|
||||
* Defaults to 'admin' for system plugins, 'site' for content plugins.
|
||||
* @return array Translations [key => value]
|
||||
*/
|
||||
public function getPluginTranslations(string $pluginName, string $lang, string $context = 'admin'): array
|
||||
{
|
||||
$langDir = $this->pluginsPath . '/' . $pluginName . '/language';
|
||||
if (!is_dir($langDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try requested language first
|
||||
$file = $langDir . '/' . $lang . '/' . $context . '.php';
|
||||
if (file_exists($file)) {
|
||||
$t = include $file;
|
||||
if (is_array($t)) {
|
||||
return $t;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the plugin's default language
|
||||
$defaultLang = $this->getPluginDefaultLanguage($pluginName);
|
||||
if ($defaultLang !== $lang) {
|
||||
$fallbackFile = $langDir . '/' . $defaultLang . '/' . $context . '.php';
|
||||
if (file_exists($fallbackFile)) {
|
||||
$t = include $fallbackFile;
|
||||
if (is_array($t)) {
|
||||
return $t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect translations for every loaded plugin for the requested language.
|
||||
*
|
||||
* Returns an associative array keyed by plugin name:
|
||||
* ['Statistics' => [...], 'Logs' => [...], ...]
|
||||
*
|
||||
* @param string $lang Requested language code
|
||||
* @param string $context 'admin' or 'site'
|
||||
* @return array Plugin translations keyed by plugin name
|
||||
*/
|
||||
public function getAllPluginTranslations(string $lang, string $context = 'admin'): array
|
||||
{
|
||||
$all = [];
|
||||
foreach ($this->plugins as $pluginName => $plugin) {
|
||||
// System plugins follow the admin language; content plugins follow the content language.
|
||||
// The caller decides the context; here we just load for every plugin.
|
||||
$all[$pluginName] = $this->getPluginTranslations($pluginName, $lang, $context);
|
||||
}
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function isPluginViewable(object $plugin): bool
|
||||
{
|
||||
if (method_exists($plugin, 'getConfig')) {
|
||||
@@ -364,7 +236,7 @@ class PluginManager
|
||||
* Find which plugin handles a given admin route.
|
||||
*
|
||||
* @param string $route The admin route (e.g. 'statistics' or 'statistics/details')
|
||||
* @return array|null ['plugin' => name, 'action' => action, 'permission' => required permission] or null
|
||||
* @return array|null ['plugin' => name, 'action' => action] or null
|
||||
*/
|
||||
public function resolveAdminRoute(string $route): ?array
|
||||
{
|
||||
@@ -376,7 +248,6 @@ class PluginManager
|
||||
return [
|
||||
'plugin' => $item['plugin'] ?? '',
|
||||
'action' => $action,
|
||||
'permission' => $item['permission'] ?? 'plugins',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+113
-3
@@ -697,6 +697,59 @@
|
||||
},
|
||||
"time": "2026-01-13T17:56:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mustache/mustache",
|
||||
"version": "v3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bobthecow/mustache.php.git",
|
||||
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
|
||||
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.19.3",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mustache\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"src/compat.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Justin Hileman",
|
||||
"email": "justin@justinhileman.info",
|
||||
"homepage": "http://justinhileman.com"
|
||||
}
|
||||
],
|
||||
"description": "A Mustache implementation in PHP.",
|
||||
"homepage": "https://github.com/bobthecow/mustache.php",
|
||||
"keywords": [
|
||||
"mustache",
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bobthecow/mustache.php/issues",
|
||||
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
|
||||
},
|
||||
"time": "2025-06-28T18:28:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/php-enum",
|
||||
"version": "1.8.5",
|
||||
@@ -914,6 +967,63 @@
|
||||
},
|
||||
"time": "2025-10-31T00:45:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-mqtt/client",
|
||||
"version": "v2.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-mqtt/client.git",
|
||||
"reference": "3d141846753a0adee265680ae073cfb9030f2390"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-mqtt/client/zipball/3d141846753a0adee265680ae073cfb9030f2390",
|
||||
"reference": "3d141846753a0adee265680ae073cfb9030f2390",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"myclabs/php-enum": "^1.7",
|
||||
"php": "^8.0",
|
||||
"psr/log": "^1.1|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/php-invoker": "^3.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-redis": "Required for the RedisRepository"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpMqtt\\Client\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marvin Mall",
|
||||
"email": "marvin-mall@msn.com",
|
||||
"role": "developer"
|
||||
}
|
||||
],
|
||||
"description": "An MQTT client written in and for PHP.",
|
||||
"keywords": [
|
||||
"client",
|
||||
"mqtt",
|
||||
"publish",
|
||||
"subscribe"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-mqtt/client/issues",
|
||||
"source": "https://github.com/php-mqtt/client/tree/v2.3.0"
|
||||
},
|
||||
"time": "2025-09-30T17:53:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/event-dispatcher",
|
||||
"version": "1.0.0",
|
||||
@@ -1727,10 +1837,10 @@
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
# CodePress CMS v2.6.1 — Release Notes
|
||||
|
||||
**Release datum:** 2026-08-17
|
||||
**Codename:** Lyra
|
||||
**Status:** stable
|
||||
|
||||
---
|
||||
|
||||
## Nieuwe features
|
||||
|
||||
### Welkomstpagina bij lege content-map (nieuwe installatie)
|
||||
- Wanneer de `content/` map leeg is (nieuwe installatie), toont de CMS nu een duidelijke welkomstpagina
|
||||
- De pagina bevat een introductietekst, een lijst met volgende stappen en knoppen naar de admin console en handleiding
|
||||
- Vertaald in NL, EN en DE (`welcome_*` keys in `language/*/site.php`)
|
||||
- `isContentDirEmpty()` filtert nu verborgen bestanden (zoals `.gitkeep`) zodat een map met alleen een `.gitkeep` als leeg wordt herkend
|
||||
|
||||
### 404-afhandeling binnen het actieve theme
|
||||
- Nieuwe statische 404 pagina in `admin/static/404.html` met een link naar de home pagina
|
||||
- De 404 inhoud wordt binnen het actieve theme gerenderd (met header, navigatie, footer en Bootstrap styling)
|
||||
- `getError404()` laadt de inhoud uit `admin/static/404.html` en geeft deze als page content terug (layout `full_content`)
|
||||
- `render()` zet de juiste `http_response_code(404)` status bij niet-gevonden pagina's
|
||||
|
||||
### HTTP 404 status bij onbekende pagina's en missende taalprefix
|
||||
- `getPage()` geeft een 404 als de URL geen geldige taalprefix bevat (bijv. `/random-page` in plaats van `/nl/random-page`)
|
||||
- Niet-bestaande pagina's binnen een taal (`/nl/nonexistent`) geven nu ook een 404 in plaats van HTTP 200
|
||||
- Eerder werd bij een lege content-map altijd de welkomstpagina getoond, ook voor niet-bestaande pagina's — dit is nu beperkt tot de home URL
|
||||
|
||||
---
|
||||
|
||||
## Verbeteringen
|
||||
|
||||
### Test scripts
|
||||
- Test scripts (`pentest.sh`, `accessibility.sh`, `run-tests.sh`) gebruiken nu de Apache-URL (`http://development.codepress.noorlander.info`) in plaats van `localhost:8080`
|
||||
- Functionele tests opgeschoond: tests die naar niet-bestaande `demo/` content pagina's verwezen vervangen door werkende tests
|
||||
|
||||
### Documentatie
|
||||
- `README.md` en `README.en.md` bijgewerkt: versie naar 2.6.1, multi-language talen gecorrigeerd (NL/EN/DE), `admin/static/` map toegevoegd aan project structuur
|
||||
- Guide index bestanden (`guide/nl/index.md`, `guide/en/index.md`) bijgewerkt naar versie 2.6.1
|
||||
- `guide/*/codepress-developer.md` versie referenties bijgewerkt naar 2.6.1
|
||||
- `AGENTS.md` samengevoegd naar de root map (`./AGENTS.md`); `./development/AGENTS.md` is verwijderd
|
||||
- AGENTS.md verduidelijkt welke URL bij welke map hoort en dat tests via Apache draaien (niet via PHP dev server)
|
||||
|
||||
---
|
||||
|
||||
## Opschoning
|
||||
|
||||
### Verwijderde ongebruikte bestanden
|
||||
- `package.json` — was voor NPM build (`npm run build:css`), runtime gebruikt scssphp (PHP) voor SCSS compilatie
|
||||
- `src/scss/` map — was voor NPM sass build, overbodig na verwijderen van `package.json`
|
||||
- `.htaccess` (root) — Apache docroot is `public/`, deze root `.htaccess` werd niet geserveerd
|
||||
- `themes/demo/` — niet actief in `config.json` (gebruikt `default`), alleen genoemd als voorbeeld
|
||||
|
||||
### Opschoning referenties
|
||||
- `README.md` en `README.en.md` — demo theme referenties verwijderd uit project structuur
|
||||
- `guide/*/codepress-developer/architectuur.md` — demo verwijdering uit mappenstructuur boom
|
||||
- `themes/gen-preview.sh` — hardcoded `default demo test` loop vervangen door dynamische `*/` loop
|
||||
- `.gitignore` — `node_modules/`, `package-lock.json` en `.sass-cache/` regels verwijderd (NPM niet meer gebruikt)
|
||||
|
||||
### Verwijderde vendor packages
|
||||
- `mustache/mustache` — niet meer gebruikt (Twig is de template engine)
|
||||
- `php-mqtt/client` — niet meer gebruikt
|
||||
- Beide packages verwijderd uit `composer.lock`, `vendor/composer/installed.json`, `installed.php`, en de autoloader bestanden
|
||||
- `MQTTTracker` referentie verwijderd uit functionele test script
|
||||
|
||||
### Installatie documentatie
|
||||
- `README.md` en `README.en.md` bevatten nu een volledige installatie sectie met:
|
||||
- Vereisten (PHP ≥8.0, composer, extensies)
|
||||
- Apache 2.4+ vhost voorbeeld met `mod_rewrite`, `mod_headers`, `AllowOverride All`
|
||||
- Nginx server block voorbeeld met PHP-FPM, clean URLs, asset-serving en security headers
|
||||
- Mappen rechten en test instructies
|
||||
|
||||
---
|
||||
|
||||
## Beveiliging
|
||||
- Pentest suite: 30/30 tests geslaagd (XSS, path traversal, PHP injection, null byte, command injection, template injection, HTTP header injection, information disclosure, security headers, DoS)
|
||||
- Security headers aanwezig: X-Frame-Options, Content-Security-Policy, X-Content-Type-Options
|
||||
- Geen vulnerabilities gedetecteerd
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
- WCAG 2.1 AA: 25/25 tests geslaagd (100%)
|
||||
- ARIA landmarks aanwezig (24 per pagina)
|
||||
- Semantic HTML structuur (header, nav, main, footer)
|
||||
- Skip-to-content links, focus indicators, screen reader support
|
||||
- Mobile viewport en touch targets aanwezig
|
||||
|
||||
---
|
||||
|
||||
## Systeem vereisten
|
||||
- PHP >= 8.0
|
||||
- Extensies: json, mbstring
|
||||
- Optioneel: opcache (aanbevolen voor performance)
|
||||
- Git (optioneel, voor content versioning feature)
|
||||
- ZipArchive (optioneel, voor ZIP backup/restore feature)
|
||||
- Apache met `mod_rewrite` en `AllowOverride All` (voor clean URLs en asset-serving)
|
||||
|
||||
---
|
||||
|
||||
## Upgraden
|
||||
1. Maak een backup van de huidige content map
|
||||
2. Pull de nieuwe code
|
||||
3. Run `composer install` om dependencies bij te werken
|
||||
4. Test de website
|
||||
5. Optioneel: initialiseer git in content/ via Admin → Backup & Restore → Git init
|
||||
|
||||
---
|
||||
|
||||
## Test resultaten samenvatting
|
||||
|
||||
| Test | Resultaat |
|
||||
|------|----------|
|
||||
| Pentest | 30/30 geslaagd, 0 vulnerabilities |
|
||||
| WCAG 2.1 AA Accessibility | 25/25 geslaagd, 100% compliance |
|
||||
@@ -1,100 +0,0 @@
|
||||
# CodePress CMS v2.6.1c — Release Notes
|
||||
|
||||
**Release datum:** 2026-08-17
|
||||
**Codename:** Lyra
|
||||
**Status:** stable
|
||||
|
||||
---
|
||||
|
||||
## Nieuwe features
|
||||
|
||||
### Admin gebruikersbeheer volledig opnieuw ontworpen
|
||||
|
||||
De admin/gebruikers pagina is volledig heringericht met drie aparte pagina's:
|
||||
|
||||
#### Gebruikerslijst (`/admin/users`)
|
||||
- Lijst met alle gebruikers (gebruikersnaam, rol, login e-mail, aanmaakdatum, acties)
|
||||
- **Zoekveld** — zoekt op gebruikersnaam, e-mail of auteur naam
|
||||
- **Rol filter** — dropdown om op een specifieke rol te filteren
|
||||
- **"Nieuwe gebruiker" knop** — linkt naar het aanmaakformulier
|
||||
- **Bewerk knop** per gebruiker — linkt naar de profiel pagina
|
||||
- **Verwijder knop** per gebruiker (bevestiging via JavaScript)
|
||||
|
||||
#### Profiel bewerken (`/admin/users-edit?user=<naam>`)
|
||||
- **Profiel gegevens**: login e-mail, auteur naam, auteur e-mail wijzigen
|
||||
- **Wachtwoord wijzigen**: nieuw wachtwoord + bevestiging met JavaScript validatie
|
||||
- **Rol wijzigen**: dropdown met alle rollen, toont huidige rol met gekleurde badge
|
||||
- **Gevarenzone**: gebruiker verwijderen (alleen voor andere gebruikers, niet jezelf)
|
||||
|
||||
#### Nieuwe gebruiker (`/admin/users-new`)
|
||||
- Formulier met gebruikersnaam, wachtwoord, e-mail, auteur naam, auteur e-mail, rol
|
||||
- Na aanmaken → automatische redirect naar profiel pagina van de nieuwe gebruiker
|
||||
|
||||
### CLI commando voor admin wachtwoord reset
|
||||
|
||||
Nieuw CLI script `cli/reset-admin-password.php` om een admin wachtwoord te resetten en brute-force lockout te wissen — handig bij lockout of vergeten wachtwoord:
|
||||
|
||||
```bash
|
||||
# Met specifiek wachtwoord
|
||||
php cli/reset-admin-password.php admin NieuwWachtwoord123
|
||||
|
||||
# Met automatisch gegenereerd wachtwoord
|
||||
php cli/reset-admin-password.php admin
|
||||
```
|
||||
|
||||
Het commando wijzigt het wachtwoord (als bcrypt hash), wist de lockout en toont het nieuwe wachtwoord.
|
||||
|
||||
---
|
||||
|
||||
## Verbeteringen
|
||||
|
||||
### AdminAuth uitbreiding
|
||||
- Nieuwe public methode `clearLockout(string $username)` — wist failed login attempts (voor CLI gebruik)
|
||||
|
||||
### Handleidingen bijgewerkt
|
||||
- `guide/nl/admin-beheerder/gebruikers.md` — volledig herschreven met nieuwe architectuur (lijst, zoeken/filter, profiel bewerken, CLI reset)
|
||||
- `guide/en/admin-beheerder/gebruikers.md` — Engelse versie bijgewerkt
|
||||
|
||||
### Vertalingen
|
||||
- 15 nieuwe admin vertaalkeys toegevoegd in NL, EN en DE (`users_list`, `search_users`, `all_roles`, `filter`, `back_to_users`, `profile_info`, `change_password`, `new_password`, `confirm_password`, `passwords_no_match`, `danger_zone`, `delete_user`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Beveiliging
|
||||
- Pentest suite: 30/30 tests geslaagd, 0 vulnerabilities
|
||||
- Security headers aanwezig: X-Frame-Options, Content-Security-Policy, X-Content-Type-Options
|
||||
- Geen vulnerabilities gedetecteerd
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
- WCAG 2.1 AA: 25/25 tests geslaagd (100%)
|
||||
- ARIA landmarks aanwezig (24 per pagina)
|
||||
- Semantic HTML structuur (header, nav, main, footer)
|
||||
- Skip-to-content links, focus indicators, screen reader support
|
||||
|
||||
---
|
||||
|
||||
## Systeem vereisten
|
||||
- PHP >= 8.0
|
||||
- Extensies: json, mbstring
|
||||
- Optioneel: opcache (aanbevolen voor performance)
|
||||
- Git (optioneel, voor content versioning feature)
|
||||
- ZipArchive (optioneel, voor ZIP backup/restore feature)
|
||||
- Apache met `mod_rewrite` en `AllowOverride All` (voor clean URLs en asset-serving)
|
||||
|
||||
---
|
||||
|
||||
## Upgraden
|
||||
1. Pull de nieuwe code
|
||||
2. Test de website
|
||||
3. Het admin wachtwoord kan gereset worden via `php cli/reset-admin-password.php admin <wachtwoord>` indien nodig
|
||||
|
||||
---
|
||||
|
||||
## Test resultaten samenvatting
|
||||
|
||||
| Test | Resultaat |
|
||||
|------|----------|
|
||||
| Pentest | 30/30 geslaagd, 0 vulnerabilities |
|
||||
| WCAG 2.1 AA Accessibility | 25/25 geslaagd, 100% compliance |
|
||||
@@ -1,173 +0,0 @@
|
||||
# CodePress CMS v2.6.1d — Release Notes
|
||||
|
||||
**Release datum:** 2026-08-18
|
||||
**Codename:** Lyra
|
||||
**Status:** stable
|
||||
|
||||
---
|
||||
|
||||
## Samenvatting
|
||||
|
||||
Deze release richt zich volledig op **plugin-verbeteringen**: plugin internationalisatie (i18n), plugin uniformiteit, en een volledig vernieuwde plugin-editor met bestandsbrowser, uploaden, verwijderen en verplaatsen van bestanden binnen een plugin.
|
||||
|
||||
---
|
||||
|
||||
## Nieuwe features
|
||||
|
||||
### 1. Plugin internationalisatie (i18n)
|
||||
|
||||
Plugins hebben nu hun eigen `language/` map met vertalingen, geïntegreerd met de admin-taal en content-taal.
|
||||
|
||||
- **Systeem plugins** (`type: "system"`) volgen de geselecteerde **admin taal** — vertalingen in `language/<lang>/admin.php`
|
||||
- **Content plugins** (`type: "content"`) volgen de geselecteerde **content taal** — vertalingen in `language/<lang>/site.php`
|
||||
- **Fallback chain**: geselecteerde taal → plugin `default_language` (uit `plugin.json`) → CMS site default → lege array (sleutel wordt ongewijzigd getoond)
|
||||
|
||||
**Core uitbreidingen:**
|
||||
- `PluginManager`: `getPluginDefaultLanguage()`, `getPluginTranslations()`, `getAllPluginTranslations()` met fallback chain
|
||||
- `PluginAPIInterface`: `getPluginTranslations()` + `t()` toegevoegd (contract voor beide API's)
|
||||
- `AdminPluginAPI`: `getAdminLanguage()`, `getPluginTranslations()`, `t()` (systeem plugins)
|
||||
- `CMSAPI`: `getPluginTranslations()`, `t()` (content plugins)
|
||||
|
||||
**Admin integratie:**
|
||||
- `public/admin.php` laadt alle plugin-vertalingen als Twig global `ta_plugins` + functions `tap()` en `plugin_menu_label()`
|
||||
- `handlePluginsConfig()` resolveert `label_key`/`help_key`/`option_label_key` via plugin-vertalingen voor instellingen-labels
|
||||
- Admin sidebar toont vertaalde plugin-menu labels via `plugin_menu_label()`
|
||||
|
||||
**Alle 6 plugins bijgewerkt:**
|
||||
- Dashboard, GeoIPInfo, Logs, Statistics (systeem): `language/nl/admin.php` + `language/en/admin.php`
|
||||
- HTMLBlock, Navigation (content): `language/nl/site.php` + `language/en/site.php`
|
||||
- `plugin.json`: `default_language: "nl"` toegevoegd
|
||||
- Logs en Statistics: instellingen-labels via `label_key`/`help_key` in plaats van hardcoded tekst
|
||||
- Alle hardcoded Nederlandse strings in `handleAdminRoute()`/`getSidebarContent()` vervangen door vertaalde waarden
|
||||
|
||||
### 2. Plugin uniformiteit
|
||||
|
||||
Elke plugin heeft nu een uniforme structuur:
|
||||
|
||||
```
|
||||
plugins/<Plugin>/
|
||||
├── <Plugin>.php # Hoofd plugin class
|
||||
├── plugin.json # Metadata + instellingen
|
||||
├── README.md # Plugin documentatie (uniform format)
|
||||
├── assets/ # CSS/JS/SCSS (.gitkeep als leeg)
|
||||
│ └── ...
|
||||
└── language/ # Vertalingen
|
||||
├── nl/
|
||||
└── en/
|
||||
```
|
||||
|
||||
- Per plugin een `README.md` met uniform format: plugin type, bestandsstructuur, functies, instellingen, taal support
|
||||
- `plugins/README.md` herschreven als korte algemene intro met plugin-overzicht tabel
|
||||
- `guide/nl|en/codepress-developer/plugin-development.md` volledig herschreven (structuur, plugin.json velden incl. `default_language`, settings schema met `label_key`/`help_key`, API methodes incl. `getPluginTranslations`/`t`, admin integratie, taal support sectie)
|
||||
- `guide/nl|en/codepress-developer/architectuur.md` bijgewerkt met plugin `language/` map
|
||||
|
||||
### 3. Plugin editor volledig vernieuwd
|
||||
|
||||
De plugin-editor (`/admin/plugins-edit`) toonde alleen het `<Plugin>.php` bestand. Nu is het een volledige bestandsbeheer-omgeving.
|
||||
|
||||
#### Bestandsbrowser zijbalk
|
||||
- Geneste, inklapbare boomstructuur van alle bestanden in de plugin-map (`.php`, `.json`, `.md`, `.html`, `.css`, `.scss`, `.js`) inclusief `language/` en `assets/` submappen
|
||||
- Mappen die het actieve bestand bevatten worden automatisch uitgeklapt
|
||||
- Iconen per bestandstype (PHP, JSON, MD, CSS, JS, HTML, mappen)
|
||||
- Bootstrap collapse via `<button>` toggles met chevron-rotatie
|
||||
- `scanPluginFiles()` helper — recursieve boom-opbouw met path-traversal bescherming
|
||||
- `editor-toolbar.js` modeMap uitgebreid: `css: 'css'`, `scss: 'css'`, `js: 'javascript'`, `json: 'javascript'`
|
||||
|
||||
#### Nieuw bestand aanmaken
|
||||
- "Nieuw bestand" knop + modal met pad-invoer (bijv. `helper.php` of `assets/css/extra.css`)
|
||||
- Submappen worden automatisch aangemaakt
|
||||
- Stub-content per extensie (`<?php\n` voor PHP, `{\n}\n` voor JSON)
|
||||
- Path-traversal bescherming: `../`/`..\\`/`./` stripping + `..`/`.` segment-guard + realpath prefix-check
|
||||
|
||||
#### Uploaden
|
||||
- Upload-knop + inklapbaar upload-formulier dat bestanden opslaat in `plugins/<naam>/assets/`
|
||||
- Toegestane extensies: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD
|
||||
- Nieuwe route: `plugins-file-upload`
|
||||
- Protected plugins (Navigation) geblokkeerd
|
||||
- Path-traversal bescherming
|
||||
|
||||
#### Verwijderen
|
||||
- Per-bestand prullenbak-icoon in de bestandsboom (altijd zichtbaar, met mouseover-tekst)
|
||||
- Nieuwe route: `plugins-file-delete`
|
||||
- Path-traversal bescherming + protected plugins geblokkeerd + dotfiles geweigerd (`.gitkeep` kan niet verwijderd worden)
|
||||
- Na verwijderen valt de editor terug op het hoofd-bestand
|
||||
|
||||
#### Verplaatsen
|
||||
- Per-bestand verplaats-icoon (pijlen) in de bestandsboom
|
||||
- Nieuwe route: `plugins-file-move` + `pages/plugins-move-form.twig`
|
||||
- Dropdown met alle mappen binnen de plugin (plugin root als optie, huidige map uitgesloten)
|
||||
- `rename()` uitvoering met path-traversal bescherming voor zowel bron als doel
|
||||
- Protected plugins geblokkeerd
|
||||
|
||||
### 4. Media invoegen in editor
|
||||
|
||||
De media-knop in de editor-toolbar was al die tijd stuk — het probeerde een niet-bestaand `#mediaModal` te openen.
|
||||
|
||||
- Nieuw JSON-endpoint `/admin/media-list`: retourneert media-bestanden (recursief), images eerst
|
||||
- Nieuwe herbruikbare Twig include `pages/_media-modal.twig`: Bootstrap modal met zoekfilter en raster van media-bestanden
|
||||
- Laadt media-lijst via `fetch('/admin/media-list')` bij openen
|
||||
- Bij klik wordt een snippet in de CodeMirror-editor ingevoegd op de cursor:
|
||||
- Markdown: ``
|
||||
- HTML/PHP: `<img src="url" alt="naam">` / `<a>` / `<video>` / `<audio>`
|
||||
- CSS/SCSS/JS/JSON: ruwe URL
|
||||
- **Plugin-context**: in de plugin-editor scant het endpoint `plugins/<naam>/assets/` i.p.v. content-map (via `?plugin=` parameter); URLs beginnen met `/plugins/<naam>/assets/`
|
||||
- `admin.twig` includeert het modal wanneer `needs_editor` true is
|
||||
- `editor-toolbar.js` media-case robuuster gemaakt
|
||||
|
||||
### 5. Plugin overzicht knoppen
|
||||
|
||||
De knoppen op de plugin-kaarten (`/admin/plugins`) tonen nu alleen iconen met mouseover-tekst (`title` + `aria-label`) in plaats van icoon + tekst, zodat ze netjes naast elkaar passen in de `btn-group w-100`.
|
||||
|
||||
---
|
||||
|
||||
## Bug fixes
|
||||
|
||||
- **Admin taal niet geïntegreerd met plugins** — opgelost via plugin i18n systeem (zie boven)
|
||||
- **Plugin editor toonde alleen `<Plugin>.php`** — opgelost via bestandsbrowser (zie boven)
|
||||
- **Media-knop in editor werkte niet** — `#mediaModal` bestond niet; opgelost via nieuwe media-modal include
|
||||
- **Plugin editor uploaden/verwijderen/verplaatsen ontbrak** — toegevoegd (zie boven)
|
||||
- **Plugin-kaart knoppen tekst liep niet goed** — teksten verwijderd, alleen iconen met title
|
||||
|
||||
---
|
||||
|
||||
## Technische details
|
||||
|
||||
### Nieuwe routes
|
||||
- `media-list` — JSON endpoint voor media-bestanden
|
||||
- `plugins-file-upload` — bestanden uploaden naar plugin assets/
|
||||
- `plugins-file-delete` — bestand verwijderen uit plugin
|
||||
- `plugins-file-move` — bestand verplaatsen binnen plugin
|
||||
|
||||
### Nieuwe bestanden
|
||||
- `admin/theme/default/views/pages/_media-modal.twig` — herbruikbare media modal
|
||||
- `admin/theme/default/views/pages/plugins-move-form.twig` — verplaats-pagina
|
||||
- `plugins/<Plugin>/language/{nl,en}/{admin,site}.php` — plugin vertalingen (per plugin)
|
||||
- `plugins/<Plugin>/README.md` — uniforme documentatie (per plugin)
|
||||
- `plugins/<Plugin>/assets/.gitkeep` — lege assets map placeholder
|
||||
|
||||
### Gewijzigde bestanden (core)
|
||||
- `cms/core/plugin/PluginManager.php` — i18n methoden, site default language
|
||||
- `cms/core/plugin/PluginAPIInterface.php` — `getPluginTranslations()`, `t()` contract
|
||||
- `cms/core/plugin/AdminPluginAPI.php` — admin language, plugin translations
|
||||
- `cms/core/plugin/CMSAPI.php` — content language plugin translations
|
||||
- `cms/core/class/CodePressCMS.php` — PluginManager + CMSAPI wiring
|
||||
- `public/admin.php` — media-list endpoint, plugin-file routes, plugin translations Twig globals, handlePluginsEdit herschreven, handlePluginsConfig label_key support, flash messages
|
||||
- `admin/theme/default/views/layouts/admin.twig` — media modal include
|
||||
- `admin/theme/default/views/pages/plugins-edit.twig` — bestandsbrowser, upload, verwijderen, verplaatsen
|
||||
- `admin/theme/default/views/pages/plugin-config.twig` — vertaalde instellingen-labels
|
||||
- `admin/theme/default/views/pages/plugins.twig` — icoon-only knoppen met titles
|
||||
- `admin/theme/default/assets/js/editor-toolbar.js` — modeMap uitgebreid, media-case robuust
|
||||
- `language/{nl,en,de}/admin.php` — nieuwe vertaalstrings
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
- **Pentest**: 30/30 tests geslaagd — 0 vulnerabilities
|
||||
- **WCAG 2.1 AA accessibility**: 25/25 tests geslaagd — 100% compliance
|
||||
|
||||
---
|
||||
|
||||
## Bestanden gewijzigd sinds v2.6.1c
|
||||
|
||||
40 bestanden gewijzigd, 2559 insertions(+), 369 deletions(-)
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
The admin manager guide contains the following topics:
|
||||
|
||||
- **Dashboard** - Role-based overview of the website (Admin, Content Manager, BI Manager, Site Admin)
|
||||
- **Content management** - Managing files and pages with layout selection and plugins
|
||||
- **Configuration** - Site settings (title, language, author, analytics, logging)
|
||||
- **Theme management** - Managing, activating and creating themes (SCSS compilation)
|
||||
- **Security** - Bot protection, rate limiting and session settings
|
||||
- **Plugins** - Managing plugins: content (sidebar) and system (admin menu/API)
|
||||
- **Users** - User management with roles (Admin, Content Manager, BI Manager, Site Admin)
|
||||
- **Statistics** - Viewing and exporting visitor statistics
|
||||
- **Dashboard** - Website overview
|
||||
- **Content management** - Managing files and pages
|
||||
- **Configuration** - Site settings
|
||||
- **Theme management** - Managing and creating themes
|
||||
- **Security** - Bot protection and sessions
|
||||
- **Plugins** - Managing and creating plugins
|
||||
- **Users** - Adding and removing users
|
||||
- **Statistics** - Viewing visitor statistics
|
||||
- **Logs** - Viewing and filtering log files
|
||||
- **Media** - Managing media files
|
||||
- **Update** - Check for updates
|
||||
|
||||
@@ -2,45 +2,16 @@
|
||||
|
||||
## Managing files
|
||||
|
||||
- **New folder** — Create folder structure
|
||||
- **New file** — Create a page (`.md`, `.php`, `.html`)
|
||||
- **Edit** — Modify existing content in the CodeMirror editor
|
||||
- **Rename** — Change file or folder names
|
||||
- **Move** — Move content to another folder
|
||||
- **Delete** — Remove content
|
||||
- **Rename folder** — Change a folder name
|
||||
- **Upload** - Upload media files
|
||||
- **New folder** - Create folder structure
|
||||
- **New file** - Create a page
|
||||
- **Edit** - Modify existing content
|
||||
- **Rename** - Change file names
|
||||
- **Move** - Move content
|
||||
- **Delete** - Remove content
|
||||
|
||||
## Editor (content-edit)
|
||||
## Editor
|
||||
|
||||
- **CodeMirror** with syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** for quickly inserting Markdown
|
||||
- **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
|
||||
|
||||
## Layout selection
|
||||
|
||||
On the content-edit page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
|
||||
|
||||
## Plugins on pages
|
||||
|
||||
- Content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
|
||||
- Choose which plugins appear in the sidebar
|
||||
- **Order is adjustable** with up/down buttons
|
||||
- The plugin order is stored in the frontmatter `plugins:` key
|
||||
- Plugins are **hidden** when the chosen layout has no sidebar (e.g. `full_content`)
|
||||
|
||||
## Frontmatter
|
||||
|
||||
The editor updates the frontmatter live when layout or plugin selection changes:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
|
||||
# Page title
|
||||
|
||||
Content...
|
||||
```
|
||||
- CodeMirror with syntax highlighting
|
||||
- Toolbar for Markdown formatting
|
||||
- Shortcuts: Ctrl+S (save), Ctrl+N (new)
|
||||
@@ -1,32 +1,10 @@
|
||||
# Dashboard
|
||||
|
||||
The dashboard shows a **role-based overview** of the website. Which widgets are visible depends on the role of the logged-in user.
|
||||
The dashboard shows an overview of:
|
||||
|
||||
## Roles and widgets
|
||||
|
||||
### Admin
|
||||
- Views (30 days) and unique visitors
|
||||
- Views (30 days)
|
||||
- Unique visitors
|
||||
- Number of pages and folders
|
||||
- Active plugins
|
||||
- Recent activity
|
||||
- System information
|
||||
- Quick actions (new page, plugin, theme)
|
||||
|
||||
### Content Manager
|
||||
- Number of pages and folders
|
||||
- Recent content changes
|
||||
- Quick actions (new page, folder)
|
||||
|
||||
### BI Manager
|
||||
- Views (30 days) and unique visitors
|
||||
- Top pages
|
||||
- Recent visits
|
||||
|
||||
### Site Admin
|
||||
- Active plugins and themes
|
||||
- System information
|
||||
- Quick actions (theme, plugin, update)
|
||||
|
||||
## Access
|
||||
|
||||
The dashboard is the default page after login (`/admin/dashboard`). All roles have access to the dashboard.
|
||||
- Quick actions
|
||||
@@ -1,80 +1,13 @@
|
||||
# Users
|
||||
|
||||
Users are stored in `admin/config/admin.json` (file-based, no database). Each user has a **role** that determines which admin routes and sidebar items are visible.
|
||||
## Add user
|
||||
|
||||
## Roles
|
||||
1. Go to **Users**
|
||||
2. Enter username
|
||||
3. Choose password
|
||||
4. Click **Add**
|
||||
|
||||
CodePress has four roles, defined in `AdminAuth::ROLE_PERMISSIONS`:
|
||||
## Delete user
|
||||
|
||||
| Role | Label | Permissions |
|
||||
|------|-------|-------------|
|
||||
| `admin` | Admin | Everything (`*`) |
|
||||
| `content-manager` | Content Manager | Content management, guide |
|
||||
| `bi-manager` | BI Manager | Statistics, logs, guide |
|
||||
| `site-admin` | Site Admin | Theme, plugins, statistics, logs, update, guide |
|
||||
|
||||
Roles are displayed with their label via `AdminAuth::ROLE_LABELS`.
|
||||
|
||||
## Users list (`/admin/users`)
|
||||
|
||||
The users list shows all users with their username, role, login email and creation date.
|
||||
|
||||
### Search and filter
|
||||
|
||||
- **Search field**: search by username, email or author name
|
||||
- **Role filter**: filter by a specific role via the dropdown
|
||||
- Click **Filter** to apply the results
|
||||
|
||||
### Adding a new user
|
||||
|
||||
1. Click **New user** (top right of the list)
|
||||
2. Enter username, password (minimum 8 characters), email, author name and author email
|
||||
3. Select a role
|
||||
4. Click **Add user**
|
||||
5. You will be automatically redirected to the profile page of the new user
|
||||
|
||||
## Editing a profile (`/admin/users-edit?user=<name>`)
|
||||
|
||||
Click on a user in the list to edit their profile. The profile page contains three sections:
|
||||
|
||||
### Profile information
|
||||
- Login email, author name and author email can be changed
|
||||
- The username cannot be changed
|
||||
|
||||
### Change password
|
||||
- Enter a new password (minimum 8 characters)
|
||||
- Confirm the password
|
||||
- The password is stored as a bcrypt hash
|
||||
|
||||
### Change role
|
||||
- Shows the current role with a colored badge
|
||||
- Select a new role from the dropdown
|
||||
- The change immediately affects the visible admin routes and sidebar items
|
||||
|
||||
### Delete user (Danger zone)
|
||||
- Only visible for other users (not for your own account)
|
||||
- Confirmation via JavaScript dialog
|
||||
- After deletion you return to the users list
|
||||
|
||||
## Admin password reset via CLI
|
||||
|
||||
If the admin is locked out (e.g. due to brute-force lockout or forgotten password), the password can be reset via the CLI:
|
||||
|
||||
```bash
|
||||
# Reset with a specific password
|
||||
php cli/reset-admin-password.php admin NewPassword123
|
||||
|
||||
# Reset with an automatically generated password
|
||||
php cli/reset-admin-password.php admin
|
||||
```
|
||||
|
||||
The command:
|
||||
- Changes the password (as a bcrypt hash)
|
||||
- Clears the brute-force lockout for the user
|
||||
- Displays the new password in the terminal
|
||||
|
||||
## Access control
|
||||
|
||||
- Route access is checked in `public/admin.php` via `AdminAuth::hasPermission()`
|
||||
- Unauthorized routes return a **403 error**
|
||||
- Sidebar items are conditionally shown via the `has_permission()` Twig function in `admin.twig`
|
||||
- Cannot delete own account
|
||||
- Confirm with password
|
||||
@@ -2,65 +2,20 @@
|
||||
|
||||
## Managing themes
|
||||
|
||||
1. Go to **Theme** in the admin menu
|
||||
2. **Activate** — Choose the active theme (stored in `config.json`)
|
||||
3. **Compile SCSS** — Process SCSS to CSS (forced)
|
||||
4. **New theme** — Create a custom theme via admin or manually
|
||||
1. Go to **Theme** in admin menu
|
||||
2. **Activate** - Choose active theme
|
||||
3. **Compile SCSS** - Process SCSS to CSS
|
||||
4. **New theme** - Create custom theme
|
||||
|
||||
## Theme structure
|
||||
|
||||
```
|
||||
themes/default/
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: full width
|
||||
├── left_sidebar.twig # Layout: sidebar on the left
|
||||
├── right_sidebar.twig # Layout: sidebar on the right
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: guide with sidebar (Navigation plugin)
|
||||
├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS source (only CSS source — manual css/theme.css must not exist)
|
||||
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
|
||||
├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
├── theme.json # Theme configuration
|
||||
├── base.twig # Main layout
|
||||
├── full_content.twig # Layouts
|
||||
├── left_sidebar.twig
|
||||
├── right_sidebar.twig
|
||||
├── partials/ # Header, nav, footer
|
||||
└── assets/ # CSS, JS, images
|
||||
```
|
||||
|
||||
## theme.json
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "default",
|
||||
"config": {
|
||||
"default_template": "full_content"
|
||||
},
|
||||
"template": {
|
||||
"full_content": "full_content.twig",
|
||||
"left_sidebar": "left_sidebar.twig",
|
||||
"right_sidebar": "right_sidebar.twig",
|
||||
"custom1": "custom1.twig",
|
||||
"guide": "guide.twig"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `title` — Display name in admin
|
||||
- `config.default_template` — Default layout for pages without a `layout:` frontmatter
|
||||
- `template` — Mapping from layout name to `.twig` file
|
||||
|
||||
## SCSS compilation
|
||||
|
||||
- `ThemeManager` compiles `assets/scss/theme.scss` at runtime to `assets/css_compiled/theme.css` via scssphp
|
||||
- `css_compiled/` is **read-only** — do not edit manually
|
||||
- `assets/css/theme.css` must **not** exist; otherwise `ThemeManager::getCssUrl()` ignores the SCSS
|
||||
- After SCSS changes: remove `assets/css_compiled/theme.css` and `.mtime` to force recompilation
|
||||
|
||||
## Layouts
|
||||
|
||||
Layouts are chosen via the frontmatter `layout:` key in content files. Unknown layouts fall back to `config.default_template` from `theme.json`.
|
||||
|
||||
## guide.twig
|
||||
|
||||
Special layout for guides. Injects the Navigation plugin into the sidebar for sidebar navigation. Automatically used for pages in the `guide/` folder.
|
||||
@@ -2,20 +2,13 @@
|
||||
|
||||
The CodePress developer guide contains the following topics:
|
||||
|
||||
- **Architecture** — CMS architecture and folder structure
|
||||
- **Core classes** — CodePressCMS, ThemeManager, PluginManager, AdminAuth
|
||||
- **Plugin development** — Plugin types (content/system), structure, API
|
||||
- **Routing** — Frontend, admin and plugin admin routing
|
||||
- **Security** — XSS, CSRF, path traversal, RBAC
|
||||
- **Testing** — Penetration, accessibility and functional tests
|
||||
- **Debugging** — Logging and cache
|
||||
- **Performance** — OPcache and SCSS caching
|
||||
|
||||
Important concepts in CodePress 2.6.1:
|
||||
|
||||
- **Plugin types** — Content plugins (sidebar) and system plugins (admin menu, routes, API)
|
||||
- **Admin plugin API** — System plugins can register admin menu items and routes
|
||||
- **SCSS compilation** — `ThemeManager` compiles SCSS to `css_compiled/` via scssphp (read-only)
|
||||
- **Asset serving** — `public/asset.php` serves themes/, admin/assets/ and plugins/ on Apache (instead of PHP dev router)
|
||||
- **Architecture** - CMS architecture and folder structure
|
||||
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
|
||||
- **Plugin development** - Developing plugins
|
||||
- **Routing** - Frontend and admin routing
|
||||
- **Security** - XSS, CSRF, path traversal
|
||||
- **Testing** - Penetration, accessibility and functional tests
|
||||
- **Debugging** - Logging and cache
|
||||
- **Performance** - OPcache and SCSS caching
|
||||
|
||||
Select a topic from the navigation on the left.
|
||||
@@ -4,23 +4,17 @@
|
||||
codepress/
|
||||
├── cms/core/ # Core engine
|
||||
│ ├── class/ # CodePressCMS, ThemeManager, Logger, Analytics
|
||||
│ ├── plugin/ # PluginManager, PluginAPIInterface, CMSAPI, AdminPluginAPI
|
||||
│ ├── plugin/ # PluginManager, CMSAPI, AdminPluginAPI
|
||||
│ ├── config.php # Config loader
|
||||
│ └── index.php # Bootstrap
|
||||
├── admin/ # Admin console
|
||||
│ ├── config/ # app.php, admin.json
|
||||
│ ├── src/ # AdminAuth
|
||||
│ └── theme/default/views/ # Twig templates
|
||||
├── themes/ # Themes (default, ...)
|
||||
├── themes/ # Themes (default, demo, ...)
|
||||
├── plugins/ # Plugins (Dashboard, HTMLBlock, Navigation, ...)
|
||||
│ └── <Plugin>/ # Each plugin has:
|
||||
│ ├── <Plugin>.php # Main class
|
||||
│ ├── plugin.json # Metadata + settings
|
||||
│ ├── README.md # Documentation
|
||||
│ ├── assets/ # CSS/JS/SCSS
|
||||
│ └── language/ # Plugin translations (nl/, en/, ...; admin.php/site.php)
|
||||
├── content/ # Content files (.md, .php, .html)
|
||||
├── language/ # Core CMS language files (nl/, en/, de/; site.php + admin.php)
|
||||
├── language/ # Language files (nl/, en/, de/)
|
||||
├── guide/ # Guides (nl/, en/)
|
||||
├── public/ # Web root
|
||||
│ ├── index.php # Website entry point
|
||||
@@ -28,7 +22,3 @@ codepress/
|
||||
├── config.json # Site configuration
|
||||
└── version.php # Version info
|
||||
```
|
||||
|
||||
## Plugin i18n
|
||||
|
||||
Plugins have their own `language/` directory with translations. System plugins follow the admin language (`language/<lang>/admin.php`), content plugins follow the content language (`language/<lang>/site.php`). Fallback chain: selected language → plugin `default_language` → CMS site default → empty array. See `guide/en/codepress-developer/plugin-development.md` for details.
|
||||
@@ -2,24 +2,14 @@
|
||||
|
||||
## Plugin structure
|
||||
|
||||
Each plugin has its own folder under `plugins/`. The folder name must match the main plugin class name.
|
||||
|
||||
```
|
||||
plugins/MyPlugin/
|
||||
├── MyPlugin.php # Main plugin class (name = folder name)
|
||||
├── plugin.json # Plugin metadata + settings schema
|
||||
├── README.md # Plugin documentation
|
||||
├── config.json # Optional runtime configuration (overrides defaults)
|
||||
├── assets/ # Optional CSS/JS/SCSS
|
||||
│ ├── css/
|
||||
│ └── scss/
|
||||
└── language/ # Translations
|
||||
├── nl/
|
||||
│ ├── admin.php # Admin labels (system plugins)
|
||||
│ └── site.php # Front-end labels (content plugins)
|
||||
└── en/
|
||||
├── admin.php
|
||||
└── site.php
|
||||
├── plugin.json # Plugin metadata
|
||||
├── config.json # Optional configuration
|
||||
└── assets/ # Optional CSS/JS
|
||||
├── css/
|
||||
└── scss/
|
||||
```
|
||||
|
||||
## plugin.json
|
||||
@@ -32,9 +22,7 @@ plugins/MyPlugin/
|
||||
"description": "Description",
|
||||
"type": "content",
|
||||
"essential": false,
|
||||
"hasConfig": false,
|
||||
"default_language": "nl",
|
||||
"settings": []
|
||||
"hasConfig": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -49,54 +37,6 @@ plugins/MyPlugin/
|
||||
| `type` | `"system"` or `"content"` | System (blue badge) or content (green badge) |
|
||||
| `essential` | boolean | Essential plugins cannot be edited/deleted |
|
||||
| `hasConfig` | boolean | Shows a Config button in admin |
|
||||
| `default_language` | string | Fallback language for plugin translations (e.g. `nl`) |
|
||||
| `settings` | array | Settings schema (see below) |
|
||||
|
||||
## Settings schema
|
||||
|
||||
When `hasConfig: true`, define settings in the `settings` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"key": "max_items",
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"label_key": "setting_max_items",
|
||||
"help_key": "setting_max_items_help"
|
||||
},
|
||||
{
|
||||
"key": "required_roles",
|
||||
"type": "multi-select",
|
||||
"default": ["admin"],
|
||||
"options": {
|
||||
"admin": "Admin",
|
||||
"content-manager": "Content Manager"
|
||||
},
|
||||
"label_key": "setting_required_roles",
|
||||
"help_key": "setting_required_roles_help",
|
||||
"option_label_key": "role_options"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Per-setting fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `key` | Setting key (stored in `config.json`) |
|
||||
| `type` | `text`, `checkbox`, `number`, `select`, `multi-select` |
|
||||
| `default` | Default value |
|
||||
| `label` | Hardcoded label (fallback when no `label_key` or when translation missing) |
|
||||
| `help` | Hardcoded help text (fallback when no `help_key`) |
|
||||
| `label_key` | Key in the plugin's `language/<lang>/admin.php` for a translated label |
|
||||
| `help_key` | Key in the plugin's `language/<lang>/admin.php` for translated help text |
|
||||
| `option_label_key` | Key pointing to an array of translated option labels for `select`/`multi-select` |
|
||||
| `options` | Options for `select`/`multi-select` (`{value: label}`) |
|
||||
|
||||
The admin loads defaults from `plugin.json` and overrides them with values from `config.json`. The plugin reads the resolved values via `PluginManager::getPluginConfig()` or its own `getPluginConfig()` method.
|
||||
|
||||
## Plugin class example
|
||||
|
||||
@@ -123,11 +63,8 @@ class MyPlugin
|
||||
|
||||
public function getSidebarContent(): string
|
||||
{
|
||||
// Fetch plugin translations (content plugin = front-end language)
|
||||
$t = $this->api ? $this->api->getPluginTranslations('MyPlugin') : [];
|
||||
$title = $this->api ? $this->api->getCurrentPageTitle() : '';
|
||||
$label = $t['current_page'] ?? 'Current page';
|
||||
return '<p>' . htmlspecialchars($label) . ': ' . htmlspecialchars($title) . '</p>';
|
||||
return '<p>Current page: ' . htmlspecialchars($title) . '</p>';
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -139,122 +76,20 @@ The plugin class is automatically loaded by `PluginManager` when the plugin is l
|
||||
The API is injected via `setAPI()`, not via a static method. In the front-end context this is a `CMSAPI` instance, in the admin context an `AdminPluginAPI` instance. Both implement `PluginAPIInterface`.
|
||||
|
||||
```php
|
||||
// Front-end API (CMSAPI) - for content plugins
|
||||
// Front-end API (CMSAPI)
|
||||
$this->api->getCurrentPageTitle();
|
||||
$this->api->getMenu();
|
||||
$this->api->getConfig('site_title');
|
||||
$this->api->getCurrentLanguage();
|
||||
$this->api->isHomepage();
|
||||
$this->api->createUrl('about-us');
|
||||
$this->api->getPluginTranslations('MyPlugin'); // front-end language
|
||||
$this->api->t('current_page', 'MyPlugin'); // translation helper
|
||||
|
||||
// Admin API (AdminPluginAPI) - for system plugins
|
||||
// Admin API (AdminPluginAPI)
|
||||
$this->api->getConfig('analytics.enabled');
|
||||
$this->api->getContentDir();
|
||||
$this->api->getEnabledPlugins();
|
||||
$this->api->getAdminLanguage(); // active admin language
|
||||
$this->api->getPluginTranslations('MyPlugin'); // admin language
|
||||
$this->api->t('menu_label', 'MyPlugin'); // translation helper
|
||||
```
|
||||
|
||||
## Language support (i18n)
|
||||
|
||||
Each plugin can provide translations in `language/<lang>/`. System plugins use `admin.php`, content plugins use `site.php`.
|
||||
|
||||
### Language file format
|
||||
|
||||
`language/en/admin.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'My Plugin',
|
||||
|
||||
// Page
|
||||
'page_title' => 'My Plugin',
|
||||
'current_page' => 'Current page',
|
||||
|
||||
// Settings (label_key/help_key point here)
|
||||
'setting_max_items' => 'Maximum number of items',
|
||||
'setting_max_items_help' => 'Number of items shown.',
|
||||
'role_options' => [
|
||||
'admin' => 'Admin',
|
||||
'content-manager' => 'Content Manager',
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Fallback chain
|
||||
|
||||
Plugin translations are resolved in this order:
|
||||
|
||||
1. **Selected language** — `language/<selected>/admin.php` (or `site.php`)
|
||||
2. **Plugin default_language** — `plugin.json` `default_language` (e.g. `nl`)
|
||||
3. **CMS site default** — `config.language.default`
|
||||
4. **Empty array** — the key is shown unchanged
|
||||
|
||||
### System vs content plugins
|
||||
|
||||
- **System plugins** (`type: "system"`) follow the selected **admin language** (`config.admin_language`). Translations live in `language/<lang>/admin.php`.
|
||||
- **Content plugins** (`type: "content"`) follow the selected **content language** (from the URL or `config.language.default`). Translations live in `language/<lang>/site.php`.
|
||||
|
||||
### Fetching translations in a plugin
|
||||
|
||||
```php
|
||||
// In handleAdminRoute() or getSidebarContent():
|
||||
$t = $this->api->getPluginTranslations('MyPlugin');
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
echo htmlspecialchars($tr('page_title'));
|
||||
```
|
||||
|
||||
Or use the single-key helper:
|
||||
|
||||
```php
|
||||
echo htmlspecialchars($this->api->t('page_title', 'MyPlugin'));
|
||||
```
|
||||
|
||||
### Translating the menu label
|
||||
|
||||
In `getAdminMenu()`, add `label_key`. The admin sidebar shows the translation via `plugin_menu_label()`:
|
||||
|
||||
```php
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'plugin' => 'MyPlugin',
|
||||
'route' => 'my-plugin',
|
||||
'label' => 'My Plugin', // fallback
|
||||
'label_key' => 'menu_label', // points to language/<lang>/admin.php
|
||||
'icon' => 'bi-puzzle',
|
||||
'section' => 'general', // or 'system'
|
||||
'permission' => 'my-plugin',
|
||||
],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Translating setting labels
|
||||
|
||||
In `plugin.json` `settings`, use `label_key`/`help_key`/`option_label_key` instead of hardcoded `label`/`help`:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "max_items",
|
||||
"label_key": "setting_max_items",
|
||||
"help_key": "setting_max_items_help",
|
||||
"type": "number",
|
||||
"default": 10
|
||||
}
|
||||
```
|
||||
|
||||
The admin's `handlePluginsConfig()` resolves these via the plugin translations; if a translation is missing it falls back to `label`/`help` from `plugin.json`.
|
||||
|
||||
## Hooks
|
||||
|
||||
Plugins can implement the following methods for automatic hook registration:
|
||||
@@ -280,72 +115,25 @@ Plugins can add custom admin pages via `getAdminMenu()` and `handleAdminRoute()`
|
||||
```php
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
$requiredRoles = $config['required_roles'] ?? ['admin'];
|
||||
|
||||
return [
|
||||
[
|
||||
'plugin' => 'MyPlugin',
|
||||
'route' => 'my-plugin',
|
||||
'label' => 'My Plugin',
|
||||
'label_key' => 'menu_label',
|
||||
'icon' => 'bi-puzzle',
|
||||
'section' => 'general', // or 'system'
|
||||
'permission' => 'my-plugin',
|
||||
'required_roles' => $requiredRoles,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function handleAdminRoute(string $action): ?string
|
||||
{
|
||||
$t = $this->api ? $this->api->getPluginTranslations('MyPlugin') : [];
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
return '<h2>' . htmlspecialchars($tr('page_title')) . '</h2>';
|
||||
return '<h2>My Plugin admin page</h2>';
|
||||
}
|
||||
```
|
||||
|
||||
Only plugins listed in `enabled_plugins` are shown in the admin sidebar.
|
||||
|
||||
### Runtime config in a plugin
|
||||
|
||||
Plugins can read their runtime config (defaults from `plugin.json` `settings` + overrides from `config.json`):
|
||||
|
||||
```php
|
||||
private function getPluginConfig(): array
|
||||
{
|
||||
$pluginDir = dirname(__DIR__);
|
||||
$pluginJsonFile = $pluginDir . '/plugin.json';
|
||||
$configJsonFile = $pluginDir . '/config.json';
|
||||
|
||||
$defaults = [];
|
||||
if (file_exists($pluginJsonFile)) {
|
||||
$pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? [];
|
||||
foreach ($pluginJson['settings'] ?? [] as $setting) {
|
||||
if (isset($setting['key'])) {
|
||||
$defaults[$setting['key']] = $setting['default'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$overrides = [];
|
||||
if (file_exists($configJsonFile)) {
|
||||
$overrides = json_decode(file_get_contents($configJsonFile), true) ?? [];
|
||||
}
|
||||
|
||||
return array_merge($defaults, $overrides);
|
||||
}
|
||||
```
|
||||
|
||||
Or via the shared method on PluginManager:
|
||||
|
||||
```php
|
||||
$config = $this->pluginManager->getPluginConfig('MyPlugin');
|
||||
```
|
||||
|
||||
## Adding CSS
|
||||
|
||||
Plugins can provide a CSS URL via `getCssUrl()`:
|
||||
@@ -356,5 +144,3 @@ public function getCssUrl(): string
|
||||
return '/plugins/MyPlugin/assets/css/style.css';
|
||||
}
|
||||
```
|
||||
|
||||
The URL is passed to the front-end template via `plugin_css_urls`.
|
||||
@@ -2,60 +2,18 @@
|
||||
|
||||
## Frontend routing
|
||||
|
||||
Frontend routing goes through `cms/router.php` (PHP dev server) or `.htaccess` (Apache). Both provide clean URLs.
|
||||
Via `cms/router.php` for PHP dev server:
|
||||
|
||||
### PHP dev server
|
||||
|
||||
Start the server with:
|
||||
|
||||
```bash
|
||||
php -S localhost:8080 cms/router.php
|
||||
```php
|
||||
// Clean URLs: /nl/page
|
||||
// Query: ?page=page&lang=nl
|
||||
```
|
||||
|
||||
`cms/router.php` serves:
|
||||
- Clean URLs: `/nl/page` → `public/index.php?page=page&lang=nl`
|
||||
- `themes/` assets
|
||||
- `admin/assets/` assets
|
||||
- `plugins/` assets
|
||||
|
||||
### Apache (live server)
|
||||
|
||||
`public/.htaccess` rewrites URLs to `public/index.php`. Asset URLs (`/themes/`, `/admin/assets/`, `/plugins/`) are forwarded to `public/asset.php`.
|
||||
|
||||
`public/asset.php` serves files from the correct folders with the correct MIME type:
|
||||
- `/themes/<name>/assets/...` → `themes/<name>/assets/...`
|
||||
- `/admin/assets/...` → `admin/theme/default/assets/...`
|
||||
- `/plugins/<name>/assets/...` → `plugins/<name>/assets/...`
|
||||
|
||||
## Admin routing
|
||||
|
||||
Admin routing goes through `public/admin.php` with clean URLs:
|
||||
|
||||
```
|
||||
/admin/dashboard → ?route=dashboard
|
||||
/admin/content → ?route=content
|
||||
/admin/plugins → ?route=plugins
|
||||
```
|
||||
|
||||
`cms/router.php` or `.htaccess` converts `/admin/<route>` to `?route=<route>`.
|
||||
|
||||
### Route access control (RBAC)
|
||||
|
||||
Each route is checked via `AdminAuth::hasPermission($route)`:
|
||||
- The `admin` role has wildcard `*` access
|
||||
- Other roles have an explicit list of allowed routes in `ROLE_PERMISSIONS`
|
||||
- Unauthorized routes return a **403 error**
|
||||
|
||||
## Plugin admin routing
|
||||
|
||||
System plugins can register admin routes. These are handled by `PluginManager::handleAdminRoute($route)`:
|
||||
|
||||
1. The system plugin registers routes via `getAdminRoutes()`
|
||||
2. `PluginManager::getAdminMenuItems()` collects admin menu items via `getAdminMenu()`
|
||||
3. On an admin request `PluginManager::handleAdminRoute($route)` finds a plugin that handles the route
|
||||
4. The plugin method `handleAdminRoute($route)` renders the page content
|
||||
Via `public/admin.php`:
|
||||
|
||||
```php
|
||||
// PluginManager calls this for /admin/my-system
|
||||
$plugin->handleAdminRoute('my-system');
|
||||
// Routes: /admin/dashboard, /admin/content, etc.
|
||||
// Query parameter: ?route=dashboard
|
||||
```
|
||||
@@ -4,48 +4,28 @@ Content is stored in the `content/` folder without a database.
|
||||
|
||||
## Supported file formats
|
||||
|
||||
- `.md` — Markdown (recommended)
|
||||
- `.php` — Dynamic PHP pages
|
||||
- `.html` — Static HTML pages
|
||||
- `.md` - Markdown (recommended)
|
||||
- `.php` - Dynamic PHP pages
|
||||
- `.html` - Static HTML pages
|
||||
|
||||
## File name conventions
|
||||
|
||||
```
|
||||
nl.pagina-naam.md # Dutch page
|
||||
en.page-name.md # English page
|
||||
nl.folder/ # Dutch folder (requires an index file to be clickable)
|
||||
nl.pagina-naam.md # Dutch page
|
||||
```
|
||||
|
||||
The language prefix is dynamically removed based on available languages via `getAvailableLanguages()`.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Markdown files can contain frontmatter metadata:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
layout: full_content
|
||||
plugins: HTMLBlock
|
||||
---
|
||||
|
||||
# Page title
|
||||
|
||||
Content...
|
||||
```
|
||||
|
||||
## Frontmatter fields
|
||||
|
||||
- `layout` — Layout name (must exist in the `theme.json` template mapping; fallback to `config.default_template`)
|
||||
- `plugins` — List of content plugins that appear in the sidebar
|
||||
|
||||
### plugins field
|
||||
|
||||
The `plugins` field determines:
|
||||
- **Which plugins** appear in the sidebar (only content plugins, not system plugins)
|
||||
- **The order** of the plugins in the sidebar (top to bottom)
|
||||
|
||||
The order in the frontmatter is the order in which the plugins are shown. In the admin content-edit page you can adjust the order with up/down buttons; the frontmatter is updated live.
|
||||
|
||||
Plugins are not shown when the chosen layout has no sidebar (e.g. `full_content`).
|
||||
@@ -5,44 +5,12 @@
|
||||
1. Login at `/admin`
|
||||
2. Go to **Content**
|
||||
3. Choose a folder or create a new page
|
||||
4. Edit content in the CodeMirror editor
|
||||
4. Edit content in the editor
|
||||
5. Save with Ctrl+S
|
||||
|
||||
## Editor features
|
||||
|
||||
- **CodeMirror** editor with syntax highlighting (Markdown, PHP, HTML)
|
||||
- **CodeMirror** editor with syntax highlighting
|
||||
- **Toolbar** for quick Markdown insertion
|
||||
- **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
|
||||
- **Live preview** via button
|
||||
- **Auto-save** backups in `.bak/` folder
|
||||
|
||||
## Layout selection
|
||||
|
||||
On the content-edit page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
---
|
||||
```
|
||||
|
||||
Unknown layouts fall back to `config.default_template` from `theme.json`.
|
||||
|
||||
## Plugin selection and order
|
||||
|
||||
On the content-edit page you can choose content plugins for the sidebar:
|
||||
|
||||
- **Selection** — Only content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
|
||||
- **Order** — Adjust the order with up/down buttons
|
||||
- **Frontmatter update** — The frontmatter `plugins:` key is updated live on changes
|
||||
- **Layout without sidebar** — Plugins are hidden when the chosen layout has no sidebar (e.g. `full_content`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
```
|
||||
|
||||
The order in the frontmatter determines the order of the plugins in the sidebar (top to bottom).
|
||||
@@ -1,12 +1,3 @@
|
||||
# Manual
|
||||
|
||||
Welcome to the CodePress CMS manual (version 2.6.1). CodePress is a file-based CMS without a database — content, configuration and users are stored in files.
|
||||
|
||||
The manual is divided into four sections:
|
||||
|
||||
- **Admin Manager** — Managing the website: dashboard, content, configuration, themes, security, plugins (content and system), users with roles, statistics and logs
|
||||
- **Content Manager** — Managing content: structure, managing pages (layout selection, plugin order), media and the Content API
|
||||
- **Theme Developer** — Developing themes: structure, `theme.json`, Twig templates, SCSS styling (only CSS source), layouts and new themes
|
||||
- **CodePress Developer** — Core development: architecture, core classes, plugin development (content/system), routing, security (RBAC), testing, debugging and performance
|
||||
|
||||
Select a topic from the navigation on the left.
|
||||
@@ -3,7 +3,7 @@
|
||||
The theme developer guide contains the following topics:
|
||||
|
||||
- **Theme structure** - Folder structure of a theme
|
||||
- **theme.json** - Configuration of a theme
|
||||
- **theme.json** - Theme configuration
|
||||
- **Twig templates** - Twig syntax, variables and blocks
|
||||
- **SCSS styling** - Compiling and writing SCSS
|
||||
- **Layouts** - Defining and using layouts
|
||||
|
||||
@@ -2,49 +2,20 @@
|
||||
|
||||
```
|
||||
themes/my-theme/
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: full width
|
||||
├── left_sidebar.twig # Layout: sidebar on the left
|
||||
├── right_sidebar.twig # Layout: sidebar on the right
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: guide with sidebar (Navigation plugin)
|
||||
├── theme.json # Theme configuration
|
||||
├── base.twig # Main layout
|
||||
├── full_content.twig # Layout: full-width
|
||||
├── left_sidebar.twig # Layout: left sidebar
|
||||
├── right_sidebar.twig # Layout: right sidebar
|
||||
├── custom.twig # Layout: custom
|
||||
├── partials/
|
||||
│ ├── header.twig
|
||||
│ ├── navigation.twig
|
||||
│ └── footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS source (the only CSS source)
|
||||
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
|
||||
├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
## SCSS is the only CSS source
|
||||
|
||||
- **ALWAYS** edit `assets/scss/theme.scss` — this is the only CSS source
|
||||
- `ThemeManager` compiles SCSS at runtime to `assets/css_compiled/theme.css` via scssphp
|
||||
- `assets/css_compiled/` is **read-only** — do not edit manually
|
||||
- **NEVER** create or edit `assets/css/theme.css` manually; this file must not exist
|
||||
- `ThemeManager::getCssUrl()` priority: 1) `assets/css/theme.css` (manual), 2) `assets/css_compiled/theme.css` (compiled). If `theme.css` exists, the SCSS is ignored.
|
||||
- After SCSS changes: remove `assets/css_compiled/theme.css` and `.mtime` to force recompilation
|
||||
|
||||
```bash
|
||||
rm themes/my-theme/assets/css_compiled/theme.css themes/my-theme/assets/css_compiled/.mtime
|
||||
```
|
||||
|
||||
## guide.twig
|
||||
|
||||
Special layout for guides. Injects the Navigation plugin into the sidebar for sidebar navigation. Automatically used for pages in the `guide/` folder.
|
||||
|
||||
## Plugin CSS loading
|
||||
|
||||
Plugin CSS is automatically loaded after theme CSS in `base.twig`, so themes can override plugin styling:
|
||||
|
||||
```twig
|
||||
{% for cssUrl in plugin_css_urls|default([]) %}
|
||||
<link href="{{ cssUrl }}" rel="stylesheet">
|
||||
{% endfor %}
|
||||
├── scss/theme.scss # SCSS source
|
||||
├── css/ # CSS files
|
||||
├── js/theme.js # JavaScript
|
||||
├── fonts/ # Fonts
|
||||
└── img/ # Images
|
||||
```
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
De admin beheerder handleiding bevat de volgende onderwerpen:
|
||||
|
||||
- **Dashboard** - Role-based overzicht van de website (Admin, Content Beheerder, BI Beheerder, Site Admin)
|
||||
- **Content beheer** - Bestanden en pagina's beheren met layout selectie en plugins
|
||||
- **Configuratie** - Site instellingen (titel, taal, auteur, analytics, logging)
|
||||
- **Thema beheer** - Thema's beheren, activeren en aanmaken (SCSS compilatie)
|
||||
- **Beveiliging** - Bot bescherming, rate limiting en sessie instellingen
|
||||
- **Plugins** - Plugins beheren: content (sidebar) en systeem (admin menu/API)
|
||||
- **Gebruikers** - Gebruikersbeheer met rollen (Admin, Content Beheerder, BI Beheerder, Site Admin)
|
||||
- **Statistieken** - Bezoekersstatistieken bekijken en exporteren
|
||||
- **Dashboard** - Overzicht van de website
|
||||
- **Content beheer** - Bestanden en pagina's beheren
|
||||
- **Configuratie** - Site instellingen
|
||||
- **Thema beheer** - Thema's beheren en aanmaken
|
||||
- **Beveiliging** - Bot bescherming en sessies
|
||||
- **Plugins** - Plugins beheren en aanmaken
|
||||
- **Gebruikers** - Gebruikers toevoegen en verwijderen
|
||||
- **Statistieken** - Bezoekersstatistieken bekijken
|
||||
- **Logs** - Logbestanden bekijken en filteren
|
||||
- **Media** - Media bestanden beheren
|
||||
- **Update** - Controleren op updates
|
||||
|
||||
@@ -2,45 +2,16 @@
|
||||
|
||||
## Bestanden beheren
|
||||
|
||||
- **Nieuwe map** — Mappen structuur aanmaken
|
||||
- **Nieuw bestand** — Pagina aanmaken (`.md`, `.php`, `.html`)
|
||||
- **Bewerken** — Bestaande content wijzigen in CodeMirror editor
|
||||
- **Hernoemen** — Bestands- of mapnamen aanpassen
|
||||
- **Verplaatsen** — Content verplaatsen naar andere map
|
||||
- **Verwijderen** — Content verwijderen
|
||||
- **Map hernoemen** — Map naam wijzigen
|
||||
- **Uploaden** - Media bestanden uploaden
|
||||
- **Nieuwe map** - Mappen structuur aanmaken
|
||||
- **Nieuw bestand** - Pagina aanmaken
|
||||
- **Bewerken** - Bestaande content wijzigen
|
||||
- **Hernoemen** - Bestandsnamen aanpassen
|
||||
- **Verplaatsen** - Content verplaatsen
|
||||
- **Verwijderen** - Content verwijderen
|
||||
|
||||
## Editor (content-edit)
|
||||
## Editor
|
||||
|
||||
- **CodeMirror** met syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** voor snel Markdown invoeren
|
||||
- **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||
|
||||
## Layout selectie
|
||||
|
||||
Op de content-edit pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
|
||||
|
||||
## Plugins op pagina's
|
||||
|
||||
- Content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
|
||||
- Kies welke plugins in de sidebar verschijnen
|
||||
- **Volgorde aanpasbaar** met up/down knoppen
|
||||
- De plugin volgorde wordt opgeslagen in de frontmatter `plugins:` key
|
||||
- Plugins worden **verbergen** als de gekozen layout geen sidebar heeft (bijv. `full_content`)
|
||||
|
||||
## Frontmatter
|
||||
|
||||
De editor werkt de frontmatter live bij bij wijzigingen van layout of plugin selectie:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
|
||||
# Pagina titel
|
||||
|
||||
Content...
|
||||
```
|
||||
- CodeMirror met syntax highlighting
|
||||
- Toolbar voor Markdown formatting
|
||||
- Sneltoetsen: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||
@@ -1,32 +1,10 @@
|
||||
# Dashboard
|
||||
|
||||
Het dashboard toont een **role-based overzicht** van de website. Welke widgets zichtbaar zijn, hangt af van de rol van de ingelogde gebruiker.
|
||||
Het dashboard toont een overzicht van:
|
||||
|
||||
## Rollen en widgets
|
||||
|
||||
### Admin
|
||||
- Weergaven (30 dagen) en unieke bezoekers
|
||||
- Weergaven (30 dagen)
|
||||
- Unieke bezoekers
|
||||
- Aantal pagina's en mappen
|
||||
- Actieve plugins
|
||||
- Recente activiteit
|
||||
- Systeem informatie
|
||||
- Snelle acties (nieuwe pagina, plugin, thema)
|
||||
|
||||
### Content Beheerder
|
||||
- Aantal pagina's en mappen
|
||||
- Recente content wijzigingen
|
||||
- Snelle acties (nieuwe pagina, map)
|
||||
|
||||
### BI Beheerder
|
||||
- Weergaven (30 dagen) en unieke bezoekers
|
||||
- Top pagina's
|
||||
- Recente bezoeken
|
||||
|
||||
### Site Admin
|
||||
- Actieve plugins en thema's
|
||||
- Systeem informatie
|
||||
- Snelle acties (thema, plugin, update)
|
||||
|
||||
## Toegang
|
||||
|
||||
Het dashboard is de standaard pagina na login (`/admin/dashboard`). Alle rollen hebben toegang tot het dashboard.
|
||||
- Snelle acties
|
||||
@@ -1,80 +1,13 @@
|
||||
# Gebruikers
|
||||
|
||||
Gebruikers worden opgeslagen in `admin/config/admin.json` (file-based, geen database). Elke gebruiker heeft een **rol** die bepaalt welke admin routes en sidebar items zichtbaar zijn.
|
||||
## Gebruiker toevoegen
|
||||
|
||||
## Rollen
|
||||
1. Ga naar **Gebruikers**
|
||||
2. Vul gebruikersnaam in
|
||||
3. Kies wachtwoord
|
||||
4. Klik **Toevoegen**
|
||||
|
||||
CodePress kent vier rollen, gedefinieerd in `AdminAuth::ROLE_PERMISSIONS`:
|
||||
## Gebruiker verwijderen
|
||||
|
||||
| Rol | Label | Permissies |
|
||||
|-----|-------|-----------|
|
||||
| `admin` | Admin | Alles (`*`) |
|
||||
| `content-manager` | Content Beheerder | Content beheer, handleiding |
|
||||
| `bi-manager` | BI Beheerder | Statistieken, logs, handleiding |
|
||||
| `site-admin` | Site Admin | Thema, plugins, statistieken, logs, update, handleiding |
|
||||
|
||||
Rollen worden getoond met hun label via `AdminAuth::ROLE_LABELS`.
|
||||
|
||||
## Gebruikerslijst (`/admin/users`)
|
||||
|
||||
De gebruikerslijst toont alle gebruikers met hun gebruikersnaam, rol, login e-mail en aanmaakdatum.
|
||||
|
||||
### Zoeken en filteren
|
||||
|
||||
- **Zoekveld**: zoek op gebruikersnaam, e-mail of auteur naam
|
||||
- **Rol filter**: filter op een specifieke rol via de dropdown
|
||||
- Klik op **Filteren** om de resultaten toe te passen
|
||||
|
||||
### Nieuwe gebruiker aanmaken
|
||||
|
||||
1. Klik op **Nieuwe gebruiker** (rechtsboven in de lijst)
|
||||
2. Vul gebruikersnaam, wachtwoord (minimaal 8 tekens), e-mail, auteur naam en auteur e-mail in
|
||||
3. Selecteer een rol
|
||||
4. Klik **Gebruiker toevoegen**
|
||||
5. Je wordt automatisch doorgestuurd naar de profiel pagina van de nieuwe gebruiker
|
||||
|
||||
## Profiel bewerken (`/admin/users-edit?user=<naam>`)
|
||||
|
||||
Klik op een gebruiker in de lijst om het profiel te bewerken. De profiel pagina bevat drie secties:
|
||||
|
||||
### Profiel gegevens
|
||||
- Login e-mail, auteur naam en auteur e-mail kunnen worden gewijzigd
|
||||
- De gebruikersnaam kan niet worden gewijzigd
|
||||
|
||||
### Wachtwoord wijzigen
|
||||
- Vul een nieuw wachtwoord in (minimaal 8 tekens)
|
||||
- Bevestig het wachtwoord
|
||||
- Het wachtwoord wordt opgeslagen als bcrypt hash
|
||||
|
||||
### Rol wijzigen
|
||||
- Toont de huidige rol met een gekleurde badge
|
||||
- Selecteer een nieuwe rol uit de dropdown
|
||||
- De wijziging beïnvloedt direct de zichtbare admin routes en sidebar items
|
||||
|
||||
### Gebruiker verwijderen (Gevarenzone)
|
||||
- Alleen zichtbaar voor andere gebruikers (niet voor je eigen account)
|
||||
- Bevestiging via JavaScript dialog
|
||||
- Na verwijderen keer je terug naar de gebruikerslijst
|
||||
|
||||
## Admin wachtwoord reset via CLI
|
||||
|
||||
Als de admin is uitgesloten (bijv. door brute-force lockout of vergeten wachtwoord), kan het wachtwoord via de CLI worden gereset:
|
||||
|
||||
```bash
|
||||
# Reset met een specifiek wachtwoord
|
||||
php cli/reset-admin-password.php admin NieuwWachtwoord123
|
||||
|
||||
# Reset met een automatisch gegenereerd wachtwoord
|
||||
php cli/reset-admin-password.php admin
|
||||
```
|
||||
|
||||
Het commando:
|
||||
- Wijzigt het wachtwoord (als bcrypt hash)
|
||||
- Wist de brute-force lockout voor de gebruiker
|
||||
- Toont het nieuwe wachtwoord in de terminal
|
||||
|
||||
## Toegangscontrole
|
||||
|
||||
- Route access wordt gecontroleerd in `public/admin.php` via `AdminAuth::hasPermission()`
|
||||
- Onbevoegde routes geven een **403 error**
|
||||
- Sidebar items worden conditioneel getoond via de `has_permission()` Twig function in `admin.twig`
|
||||
- Kan niet voor eigen account
|
||||
- Bevestig met wachtwoord
|
||||
@@ -3,64 +3,17 @@
|
||||
## Thema's beheren
|
||||
|
||||
1. Ga naar **Thema** in admin menu
|
||||
2. **Activeren** — Kies actief thema (wordt opgeslagen in `config.json`)
|
||||
3. **SCSS compileren** — Verwerk SCSS naar CSS (geforceerd)
|
||||
4. **Nieuw thema** — Eigen thema aanmaken via admin of handmatig
|
||||
2. **Activeren** - Kies actief thema
|
||||
3. **SCSS compileren** - Verwerk SCSS naar CSS
|
||||
4. **Nieuw thema** - Eigen thema aanmaken
|
||||
|
||||
## Thema structuur
|
||||
|
||||
```
|
||||
themes/default/
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: volledige breedte
|
||||
├── left_sidebar.twig # Layout: sidebar links
|
||||
├── right_sidebar.twig # Layout: sidebar rechts
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: handleiding met sidebar (Navigation plugin)
|
||||
├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS bron (enige CSS bron — handmatige css/theme.css mag niet bestaan)
|
||||
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
|
||||
├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
├── theme.json # Thema configuratie
|
||||
├── base.twig # Hoofd layout
|
||||
├── full_content.twig # Layouts
|
||||
├── partials/ # Header, nav, footer
|
||||
└── assets/ # CSS, JS, afbeeldingen
|
||||
```
|
||||
|
||||
## theme.json
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "default",
|
||||
"config": {
|
||||
"default_template": "full_content"
|
||||
},
|
||||
"template": {
|
||||
"full_content": "full_content.twig",
|
||||
"left_sidebar": "left_sidebar.twig",
|
||||
"right_sidebar": "right_sidebar.twig",
|
||||
"custom1": "custom1.twig",
|
||||
"guide": "guide.twig"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `title` — Weergavenaam in admin
|
||||
- `config.default_template` — Standaard layout voor pagina's zonder `layout:` frontmatter
|
||||
- `template` — Mapping van layout naam naar `.twig` bestand
|
||||
|
||||
## SCSS compilatie
|
||||
|
||||
- `ThemeManager` compileert `assets/scss/theme.scss` runtime naar `assets/css_compiled/theme.css` via scssphp
|
||||
- `css_compiled/` is **read-only** — niet handmatig aanpassen
|
||||
- `assets/css/theme.css` mag **niet** bestaan; anders negeert `ThemeManager::getCssUrl()` de SCSS
|
||||
- Na SCSS wijzigingen: verwijder `assets/css_compiled/theme.css` en `.mtime` om te forceren
|
||||
|
||||
## Layouts
|
||||
|
||||
Layouts worden gekozen via frontmatter `layout:` key in content bestanden. Onbekende layouts vallen terug op `config.default_template` uit `theme.json`.
|
||||
|
||||
## guide.twig
|
||||
|
||||
Speciale layout voor handleidingen. Injecteert de Navigation plugin in de sidebar voor zijbalk navigatie. Wordt automatisch gebruikt voor pagina's in de `guide/` map.
|
||||
@@ -2,20 +2,13 @@
|
||||
|
||||
De CodePress developer handleiding bevat de volgende onderwerpen:
|
||||
|
||||
- **Architectuur** — CMS architectuur en mappenstructuur
|
||||
- **Core classes** — CodePressCMS, ThemeManager, PluginManager, AdminAuth
|
||||
- **Plugin development** — Plugin types (content/systeem), structuur, API
|
||||
- **Routing** — Frontend, admin en plugin admin routing
|
||||
- **Beveiliging** — XSS, CSRF, path traversal, RBAC
|
||||
- **Testing** — Penetratie, accessibility en functionele tests
|
||||
- **Debugging** — Logging en cache
|
||||
- **Performance** — OPcache en SCSS caching
|
||||
|
||||
Belangrijke concepten in CodePress 2.6.1:
|
||||
|
||||
- **Plugin types** — Content plugins (sidebar) en systeem plugins (admin menu, routes, API)
|
||||
- **Admin plugin API** — Systeem plugins kunnen admin menu items en routes registreren
|
||||
- **SCSS compilatie** — `ThemeManager` compileert SCSS naar `css_compiled/` via scssphp (read-only)
|
||||
- **Asset serving** — `public/asset.php` serveert themes/, admin/assets/ en plugins/ op Apache (i.p.v. PHP dev router)
|
||||
- **Architectuur** - CMS architectuur en mappenstructuur
|
||||
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
|
||||
- **Plugin development** - Plugins ontwikkelen
|
||||
- **Routing** - Frontend en admin routing
|
||||
- **Beveiliging** - XSS, CSRF, path traversal
|
||||
- **Testing** - Penetratie, accessibility en functionele tests
|
||||
- **Debugging** - Logging en cache
|
||||
- **Performance** - OPcache en SCSS caching
|
||||
|
||||
Selecteer een onderwerp uit de navigatie aan de linkerkant.
|
||||
@@ -4,23 +4,17 @@
|
||||
codepress/
|
||||
├── cms/core/ # Core engine
|
||||
│ ├── class/ # CodePressCMS, ThemeManager, Logger, Analytics
|
||||
│ ├── plugin/ # PluginManager, PluginAPIInterface, CMSAPI, AdminPluginAPI
|
||||
│ ├── plugin/ # PluginManager, CMSAPI, AdminPluginAPI
|
||||
│ ├── config.php # Config loader
|
||||
│ └── index.php # Bootstrap
|
||||
├── admin/ # Admin console
|
||||
│ ├── config/ # app.php, admin.json
|
||||
│ ├── src/ # AdminAuth
|
||||
│ └── theme/default/views/ # Twig templates
|
||||
├── themes/ # Thema's (default, ...)
|
||||
├── themes/ # Thema's (default, demo, ...)
|
||||
├── plugins/ # Plugins (Dashboard, HTMLBlock, Navigation, ...)
|
||||
│ └── <Plugin>/ # Elke plugin heeft:
|
||||
│ ├── <Plugin>.php # Hoofd class
|
||||
│ ├── plugin.json # Metadata + instellingen
|
||||
│ ├── README.md # Documentatie
|
||||
│ ├── assets/ # CSS/JS/SCSS
|
||||
│ └── language/ # Plugin vertalingen (nl/, en/, ...; admin.php/site.php)
|
||||
├── content/ # Content bestanden (.md, .php, .html)
|
||||
├── language/ # Core CMS taalbestanden (nl/, en/, de/; site.php + admin.php)
|
||||
├── language/ # Taalbestanden (nl/, en/, de/)
|
||||
├── guide/ # Handleidingen (nl/, en/)
|
||||
├── public/ # Web root
|
||||
│ ├── index.php # Website entry point
|
||||
@@ -28,7 +22,3 @@ codepress/
|
||||
├── config.json # Site configuratie
|
||||
└── version.php # Versie info
|
||||
```
|
||||
|
||||
## Plugin i18n
|
||||
|
||||
Plugins hebben hun eigen `language/` map met vertalingen. Systeem plugins volgen de admin taal (`language/<lang>/admin.php`), content plugins volgen de content taal (`language/<lang>/site.php`). Fallback chain: geselecteerde taal → plugin `default_language` → CMS site default → lege array. Zie `guide/nl/codepress-developer/plugin-development.md` voor details.
|
||||
@@ -2,24 +2,14 @@
|
||||
|
||||
## Plugin structuur
|
||||
|
||||
Elke plugin heeft zijn eigen map onder `plugins/`. De mapnaam moet overeenkomen met de hoofd plugin class naam.
|
||||
|
||||
```
|
||||
plugins/MijnPlugin/
|
||||
├── MijnPlugin.php # Hoofd plugin class (naam = mapnaam)
|
||||
├── plugin.json # Plugin metadata + instellingen-schema
|
||||
├── README.md # Plugin documentatie
|
||||
├── config.json # Optionele runtime configuratie (overschrijft defaults)
|
||||
├── assets/ # Optionele CSS/JS/SCSS
|
||||
│ ├── css/
|
||||
│ └── scss/
|
||||
└── language/ # Vertalingen
|
||||
├── nl/
|
||||
│ ├── admin.php # Admin labels (systeem plugins)
|
||||
│ └── site.php # Front-end labels (content plugins)
|
||||
└── en/
|
||||
├── admin.php
|
||||
└── site.php
|
||||
├── plugin.json # Plugin metadata
|
||||
├── config.json # Optionele configuratie
|
||||
└── assets/ # Optionele CSS/JS
|
||||
├── css/
|
||||
└── scss/
|
||||
```
|
||||
|
||||
## plugin.json
|
||||
@@ -32,9 +22,7 @@ plugins/MijnPlugin/
|
||||
"description": "Beschrijving",
|
||||
"type": "content",
|
||||
"essential": false,
|
||||
"hasConfig": false,
|
||||
"default_language": "nl",
|
||||
"settings": []
|
||||
"hasConfig": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,55 +36,7 @@ plugins/MijnPlugin/
|
||||
| `description` | string | Korte beschrijving |
|
||||
| `type` | `"system"` of `"content"` | Systeem (blauwe badge) of content (groene badge) |
|
||||
| `essential` | boolean | Essentiële plugins kunnen niet worden bewerkt/verwijderd |
|
||||
| `hasConfig` | boolean | Toont een Config-knop in admin |
|
||||
| `default_language` | string | Fallback taal voor plugin-vertalingen (bijv. `nl`) |
|
||||
| `settings` | array | Instellingen-schema (zie hieronder) |
|
||||
|
||||
## Instellingen-schema
|
||||
|
||||
Als `hasConfig: true`, definieer je instellingen in het `settings` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"key": "max_items",
|
||||
"type": "number",
|
||||
"default": 10,
|
||||
"label_key": "setting_max_items",
|
||||
"help_key": "setting_max_items_help"
|
||||
},
|
||||
{
|
||||
"key": "required_roles",
|
||||
"type": "multi-select",
|
||||
"default": ["admin"],
|
||||
"options": {
|
||||
"admin": "Admin",
|
||||
"content-manager": "Content Beheerder"
|
||||
},
|
||||
"label_key": "setting_required_roles",
|
||||
"help_key": "setting_required_roles_help",
|
||||
"option_label_key": "role_options"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Per-instelling velden
|
||||
|
||||
| Veld | Beschrijving |
|
||||
|------|--------------|
|
||||
| `key` | Instelling-sleutel (opgeslagen in `config.json`) |
|
||||
| `type` | `text`, `checkbox`, `number`, `select`, `multi-select` |
|
||||
| `default` | Standaardwaarde |
|
||||
| `label` | Hardcoded label (fallback als geen `label_key` of als vertaling ontbreekt) |
|
||||
| `help` | Hardcoded help-tekst (fallback als geen `help_key`) |
|
||||
| `label_key` | Sleutel in plugin `language/<lang>/admin.php` voor vertaald label |
|
||||
| `help_key` | Sleutel in plugin `language/<lang>/admin.php` voor vertaalde help-tekst |
|
||||
| `option_label_key` | Sleutel naar een array met vertaalde optie-labels voor `select`/`multi-select` |
|
||||
| `options` | Opties voor `select`/`multi-select` (`{waarde: label}`) |
|
||||
|
||||
De admin laadt defaults uit `plugin.json` en overschrijft ze met waarden uit `config.json`. De plugin leest de uiteindelijke waarden via `PluginManager::getPluginConfig()` of een eigen `getPluginConfig()` methode.
|
||||
| `hasConfig` | boolean | Toont een Config-knop in de admin |
|
||||
|
||||
## Plugin class voorbeeld
|
||||
|
||||
@@ -123,141 +63,36 @@ class MijnPlugin
|
||||
|
||||
public function getSidebarContent(): string
|
||||
{
|
||||
// Plugin vertalingen ophalen (content plugin = front-end taal)
|
||||
$t = $this->api ? $this->api->getPluginTranslations('MijnPlugin') : [];
|
||||
$title = $this->api ? $this->api->getCurrentPageTitle() : '';
|
||||
$label = $t['current_page'] ?? 'Huidige pagina';
|
||||
return '<p>' . htmlspecialchars($label) . ': ' . htmlspecialchars($title) . '</p>';
|
||||
return '<p>Huidige pagina: ' . htmlspecialchars($title) . '</p>';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
De plugin class wordt automatisch geladen door `PluginManager` als de plugin in `enabled_plugins` staat in `config.json`.
|
||||
|
||||
## API gebruiken
|
||||
## CMSAPI gebruiken
|
||||
|
||||
De API wordt geïnjecteerd via `setAPI()`. In de front-end context is dit een `CMSAPI` instance, in de admin context een `AdminPluginAPI` instance. Beide implementeren `PluginAPIInterface`.
|
||||
De API wordt geïnjecteerd via `setAPI()`, niet via een statische methode. In de front-end context is dit een `CMSAPI` instance, in de admin context een `AdminPluginAPI` instance. Beide implementeren `PluginAPIInterface`.
|
||||
|
||||
```php
|
||||
// Front-end API (CMSAPI) - voor content plugins
|
||||
// Front-end API (CMSAPI)
|
||||
$this->api->getCurrentPageTitle();
|
||||
$this->api->getMenu();
|
||||
$this->api->getConfig('site_title');
|
||||
$this->api->getCurrentLanguage();
|
||||
$this->api->isHomepage();
|
||||
$this->api->createUrl('over-ons');
|
||||
$this->api->getPluginTranslations('MijnPlugin'); // front-end taal
|
||||
$this->api->t('current_page', 'MijnPlugin'); // vertaalhelper
|
||||
|
||||
// Admin API (AdminPluginAPI) - voor systeem plugins
|
||||
// Admin API (AdminPluginAPI)
|
||||
$this->api->getConfig('analytics.enabled');
|
||||
$this->api->getContentDir();
|
||||
$this->api->getEnabledPlugins();
|
||||
$this->api->getAdminLanguage(); // actieve admin taal
|
||||
$this->api->getPluginTranslations('MijnPlugin'); // admin taal
|
||||
$this->api->t('menu_label', 'MijnPlugin'); // vertaalhelper
|
||||
```
|
||||
|
||||
## Taal support (i18n)
|
||||
|
||||
Elke plugin kan vertalingen leveren in `language/<lang>/`. Systeem plugins gebruiken `admin.php`, content plugins gebruiken `site.php`.
|
||||
|
||||
### Taalbestand formaat
|
||||
|
||||
`language/nl/admin.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'Mijn Plugin',
|
||||
|
||||
// Pagina
|
||||
'page_title' => 'Mijn Plugin',
|
||||
'current_page' => 'Huidige pagina',
|
||||
|
||||
// Instellingen (label_key/help_key verwijzen hiernaar)
|
||||
'setting_max_items' => 'Maximaal aantal items',
|
||||
'setting_max_items_help' => 'Aantal items dat getoond wordt.',
|
||||
'role_options' => [
|
||||
'admin' => 'Admin',
|
||||
'content-manager' => 'Content Beheerder',
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Fallback chain
|
||||
|
||||
Plugin-vertalingen worden opgelost in deze volgorde:
|
||||
|
||||
1. **Geselecteerde taal** — `language/<geselecteerd>/admin.php` (of `site.php`)
|
||||
2. **Plugin default_language** — `plugin.json` `default_language` (bijv. `nl`)
|
||||
3. **CMS site default** — `config.language.default`
|
||||
4. **Lege array** — de sleutel wordt ongewijzigd getoond
|
||||
|
||||
### Systeem vs content plugins
|
||||
|
||||
- **Systeem plugins** (`type: "system"`) volgen de geselecteerde **admin taal** (`config.admin_language`). Vertalingen staan in `language/<lang>/admin.php`.
|
||||
- **Content plugins** (`type: "content"`) volgen de geselecteerde **content taal** (uit de URL of `config.language.default`). Vertalingen staan in `language/<lang>/site.php`.
|
||||
|
||||
### Vertalingen ophalen in een plugin
|
||||
|
||||
```php
|
||||
// In handleAdminRoute() of getSidebarContent():
|
||||
$t = $this->api->getPluginTranslations('MijnPlugin');
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
echo htmlspecialchars($tr('page_title'));
|
||||
```
|
||||
|
||||
Of gebruik de enkele-sleutel helper:
|
||||
|
||||
```php
|
||||
echo htmlspecialchars($this->api->t('page_title', 'MijnPlugin'));
|
||||
```
|
||||
|
||||
### Menu label vertalen
|
||||
|
||||
In `getAdminMenu()`, voeg `label_key` toe. De admin sidebar toont de vertaling via `plugin_menu_label()`:
|
||||
|
||||
```php
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'plugin' => 'MijnPlugin',
|
||||
'route' => 'mijn-plugin',
|
||||
'label' => 'Mijn Plugin', // fallback
|
||||
'label_key' => 'menu_label', // verwijst naar language/<lang>/admin.php
|
||||
'icon' => 'bi-puzzle',
|
||||
'section' => 'general', // of 'system'
|
||||
'permission' => 'mijn-plugin',
|
||||
],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Instellingen-labels vertalen
|
||||
|
||||
In `plugin.json` `settings`, gebruik `label_key`/`help_key`/`option_label_key` in plaats van hardcoded `label`/`help`:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "max_items",
|
||||
"label_key": "setting_max_items",
|
||||
"help_key": "setting_max_items_help",
|
||||
"type": "number",
|
||||
"default": 10
|
||||
}
|
||||
```
|
||||
|
||||
De admin `handlePluginsConfig()` lost deze op via de plugin-vertalingen; als een vertaling ontbreekt valt het terug op `label`/`help` uit `plugin.json`.
|
||||
|
||||
## Hooks
|
||||
|
||||
Plugins kunnen de volgende methodes implementeren voor automatische hook-registratie:
|
||||
Plugins kunnen de volgende methodes implementeren voor automatiche hook-registratie:
|
||||
|
||||
**Actions** (geen return waarde):
|
||||
|
||||
@@ -280,72 +115,25 @@ Plugins kunnen eigen admin-pagina's toevoegen via `getAdminMenu()` en `handleAdm
|
||||
```php
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
$requiredRoles = $config['required_roles'] ?? ['admin'];
|
||||
|
||||
return [
|
||||
[
|
||||
'plugin' => 'MijnPlugin',
|
||||
'route' => 'mijn-plugin',
|
||||
'label' => 'Mijn Plugin',
|
||||
'label_key' => 'menu_label',
|
||||
'icon' => 'bi-puzzle',
|
||||
'section' => 'general', // of 'system'
|
||||
'permission' => 'mijn-plugin',
|
||||
'required_roles' => $requiredRoles,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function handleAdminRoute(string $action): ?string
|
||||
{
|
||||
$t = $this->api ? $this->api->getPluginTranslations('MijnPlugin') : [];
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
return '<h2>' . htmlspecialchars($tr('page_title')) . '</h2>';
|
||||
return '<h2>Mijn Plugin admin pagina</h2>';
|
||||
}
|
||||
```
|
||||
|
||||
Alleen plugins die in `enabled_plugins` staan worden in de admin sidebar getoond.
|
||||
|
||||
### Runtime config in een plugin
|
||||
|
||||
Plugins kunnen hun runtime config ophalen (defaults uit `plugin.json` `settings` + overrides uit `config.json`):
|
||||
|
||||
```php
|
||||
private function getPluginConfig(): array
|
||||
{
|
||||
$pluginDir = dirname(__DIR__);
|
||||
$pluginJsonFile = $pluginDir . '/plugin.json';
|
||||
$configJsonFile = $pluginDir . '/config.json';
|
||||
|
||||
$defaults = [];
|
||||
if (file_exists($pluginJsonFile)) {
|
||||
$pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? [];
|
||||
foreach ($pluginJson['settings'] ?? [] as $setting) {
|
||||
if (isset($setting['key'])) {
|
||||
$defaults[$setting['key']] = $setting['default'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$overrides = [];
|
||||
if (file_exists($configJsonFile)) {
|
||||
$overrides = json_decode(file_get_contents($configJsonFile), true) ?? [];
|
||||
}
|
||||
|
||||
return array_merge($defaults, $overrides);
|
||||
}
|
||||
```
|
||||
|
||||
Of via de gedeelde methode op PluginManager:
|
||||
|
||||
```php
|
||||
$config = $this->pluginManager->getPluginConfig('MijnPlugin');
|
||||
```
|
||||
|
||||
## CSS toevoegen
|
||||
|
||||
Plugins kunnen een CSS-URL leveren via `getCssUrl()`:
|
||||
@@ -356,5 +144,3 @@ public function getCssUrl(): string
|
||||
return '/plugins/MijnPlugin/assets/css/style.css';
|
||||
}
|
||||
```
|
||||
|
||||
De URL wordt doorgegeven aan de front-end template via `plugin_css_urls`.
|
||||
@@ -2,60 +2,18 @@
|
||||
|
||||
## Frontend routing
|
||||
|
||||
Frontend routing verloopt via `cms/router.php` (PHP dev server) of `.htaccess` (Apache). Beide zorgen voor clean URLs.
|
||||
Via `cms/router.php` voor PHP dev server:
|
||||
|
||||
### PHP dev server
|
||||
|
||||
Start de server met:
|
||||
|
||||
```bash
|
||||
php -S localhost:8080 cms/router.php
|
||||
```php
|
||||
// Schone URLs: /nl/pagina
|
||||
// Query: ?page=pagina&lang=nl
|
||||
```
|
||||
|
||||
`cms/router.php` serveert:
|
||||
- Schone URLs: `/nl/pagina` → `public/index.php?page=pagina&lang=nl`
|
||||
- `themes/` assets
|
||||
- `admin/assets/` assets
|
||||
- `plugins/` assets
|
||||
|
||||
### Apache (live server)
|
||||
|
||||
`public/.htaccess` herschrijft URLs naar `public/index.php`. Asset URLs (`/themes/`, `/admin/assets/`, `/plugins/`) worden doorgestuurd naar `public/asset.php`.
|
||||
|
||||
`public/asset.php` serveert bestanden vanuit de juiste mappen met het juiste MIME-type:
|
||||
- `/themes/<name>/assets/...` → `themes/<name>/assets/...`
|
||||
- `/admin/assets/...` → `admin/theme/default/assets/...`
|
||||
- `/plugins/<name>/assets/...` → `plugins/<name>/assets/...`
|
||||
|
||||
## Admin routing
|
||||
|
||||
Admin routing verloopt via `public/admin.php` met clean URLs:
|
||||
|
||||
```
|
||||
/admin/dashboard → ?route=dashboard
|
||||
/admin/content → ?route=content
|
||||
/admin/plugins → ?route=plugins
|
||||
```
|
||||
|
||||
`cms/router.php` of `.htaccess` zet `/admin/<route>` om naar `?route=<route>`.
|
||||
|
||||
### Route access control (RBAC)
|
||||
|
||||
Elke route wordt gecontroleerd via `AdminAuth::hasPermission($route)`:
|
||||
- `admin` rol heeft wildcard `*` toegang
|
||||
- Andere rollen hebben een expliciete lijst van toegestane routes in `ROLE_PERMISSIONS`
|
||||
- Onbevoegde routes geven een **403 error**
|
||||
|
||||
## Plugin admin routing
|
||||
|
||||
Systeem plugins kunnen admin routes registreren. Deze worden afgehandeld door `PluginManager::handleAdminRoute($route)`:
|
||||
|
||||
1. Systeem plugin registreert routes via `getAdminRoutes()`
|
||||
2. `PluginManager::getAdminMenuItems()` verzamelt admin menu items via `getAdminMenu()`
|
||||
3. Bij een admin request zoekt `PluginManager::handleAdminRoute($route)` een plugin die de route afhandelt
|
||||
4. De plugin methode `handleAdminRoute($route)` rendert de pagina inhoud
|
||||
Via `public/admin.php`:
|
||||
|
||||
```php
|
||||
// PluginManager roept deze aan voor /admin/mijn-systeem
|
||||
$plugin->handleAdminRoute('mijn-systeem');
|
||||
// Routes: /admin/dashboard, /admin/content, etc.
|
||||
// Query parameter: ?route=dashboard
|
||||
```
|
||||
@@ -4,48 +4,28 @@ Content wordt opgeslagen in de `content/` map zonder database.
|
||||
|
||||
## Ondersteunde bestandsformaten
|
||||
|
||||
- `.md` — Markdown (aanbevolen)
|
||||
- `.php` — Dynamische PHP pagina's
|
||||
- `.html` — Statische HTML pagina's
|
||||
- `.md` - Markdown (aanbevolen)
|
||||
- `.php` - Dynamische PHP pagina's
|
||||
- `.html` - Statische HTML pagina's
|
||||
|
||||
## Bestandsnaam conventies
|
||||
|
||||
```
|
||||
nl.pagina-naam.md # Nederlandse pagina
|
||||
en.page-name.md # Engelse pagina
|
||||
nl.map/ # Nederlandse map (vereist index bestand om klikbaar te zijn)
|
||||
```
|
||||
|
||||
Taalprefix wordt dynamisch verwijderd op basis van beschikbare talen via `getAvailableLanguages()`.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Markdown bestanden kunnen frontmatter metadata bevatten:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
layout: full_content
|
||||
plugins: HTMLBlock
|
||||
---
|
||||
|
||||
# Pagina titel
|
||||
|
||||
Content...
|
||||
```
|
||||
|
||||
## Frontmatter velden
|
||||
|
||||
- `layout` — Layout naam (moet voorkomen in `theme.json` template mapping; fallback op `config.default_template`)
|
||||
- `plugins` — Lijst van content plugins die in de sidebar verschijnen
|
||||
|
||||
### plugins veld
|
||||
|
||||
Het `plugins` veld bepaalt:
|
||||
- **Welke plugins** in de sidebar verschijnen (alleen content plugins, niet systeem plugins)
|
||||
- **De volgorde** van de plugins in de sidebar (top tot onder)
|
||||
|
||||
De volgorde in de frontmatter is de volgorde waarin de plugins worden getoond. In de admin content-edit pagina kun je de volgorde aanpassen met up/down knoppen; de frontmatter wordt live bijgewerkt.
|
||||
|
||||
Plugins worden niet getoond als de gekozen layout geen sidebar heeft (bijv. `full_content`).
|
||||
@@ -5,44 +5,12 @@
|
||||
1. Login op `/admin`
|
||||
2. Ga naar **Content**
|
||||
3. Kies een map of maak een nieuwe pagina
|
||||
4. Bewerk content in de CodeMirror editor
|
||||
4. Bewerk content in de editor
|
||||
5. Sla op met Ctrl+S
|
||||
|
||||
## Editor functies
|
||||
|
||||
- **CodeMirror** editor met syntax highlighting (Markdown, PHP, HTML)
|
||||
- **CodeMirror** editor met syntax highlighting
|
||||
- **Toolbar** voor snel Markdown invoegen
|
||||
- **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||
- **Live preview** via knop
|
||||
- **Auto-save** backups in `.bak/` map
|
||||
|
||||
## Layout selectie
|
||||
|
||||
Op de content-edit pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
---
|
||||
```
|
||||
|
||||
Onbekende layouts vallen terug op `config.default_template` uit `theme.json`.
|
||||
|
||||
## Plugin selectie en volgorde
|
||||
|
||||
Op de content-edit pagina kun je content plugins kiezen voor de sidebar:
|
||||
|
||||
- **Selectie** — Alleen content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
|
||||
- **Volgorde** — Pas de volgorde aan met up/down knoppen
|
||||
- **Frontmatter update** — De frontmatter `plugins:` key wordt live bijgewerkt bij wijzigingen
|
||||
- **Layout zonder sidebar** — Plugins worden verborgen als de gekozen layout geen sidebar heeft (bijv. `full_content`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
```
|
||||
|
||||
De volgorde in de frontmatter bepaalt de volgorde van de plugins in de sidebar (top tot onder).
|
||||
@@ -1,12 +1,3 @@
|
||||
# Handleiding
|
||||
|
||||
Welkom bij de CodePress CMS handleiding (versie 2.6.1). CodePress is een file-based CMS zonder database — content, configuratie en gebruikers worden opgeslagen in bestanden.
|
||||
|
||||
De handleiding is verdeeld in vier secties:
|
||||
|
||||
- **Admin Beheerder** — Beheer van de website: dashboard, content, configuratie, thema's, beveiliging, plugins (content en systeem), gebruikers met rollen, statistieken en logs
|
||||
- **Content Beheerder** — Content beheren: structuur, pagina's beheren (layout selectie, plugin volgorde), media en de Content API
|
||||
- **Theme Developer** — Thema's ontwikkelen: structuur, `theme.json`, Twig templates, SCSS styling (enige CSS bron), layouts en nieuwe thema's
|
||||
- **CodePress Developer** — Core ontwikkeling: architectuur, core classes, plugin development (content/systeem), routing, beveiliging (RBAC), testing, debugging en performance
|
||||
|
||||
Selecteer een onderwerp uit de navigatie aan de linkerkant.
|
||||
@@ -2,49 +2,20 @@
|
||||
|
||||
```
|
||||
themes/mijn-thema/
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: volledige breedte
|
||||
├── theme.json # Thema configuratie
|
||||
├── base.twig # Hoofd layout
|
||||
├── full_content.twig # Layout: full-width
|
||||
├── left_sidebar.twig # Layout: sidebar links
|
||||
├── right_sidebar.twig # Layout: sidebar rechts
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: handleiding met sidebar (Navigation plugin)
|
||||
├── custom.twig # Layout: custom
|
||||
├── partials/
|
||||
│ ├── header.twig
|
||||
│ ├── navigation.twig
|
||||
│ └── footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS bron (de enige CSS bron)
|
||||
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
|
||||
├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
## SCSS is de enige CSS bron
|
||||
|
||||
- **ALTIJD** `assets/scss/theme.scss` aanpassen — dit is de enige CSS bron
|
||||
- `ThemeManager` compileert SCSS runtime naar `assets/css_compiled/theme.css` via scssphp
|
||||
- `assets/css_compiled/` is **read-only** — niet handmatig aanpassen
|
||||
- **NOOIT** `assets/css/theme.css` handmatig aanmaken of aanpassen; dit bestand mag niet bestaan
|
||||
- `ThemeManager::getCssUrl()` prioriteit: 1) `assets/css/theme.css` (handmatig), 2) `assets/css_compiled/theme.css` (gecompileerd). Als `theme.css` bestaat, wordt de SCSS negeren.
|
||||
- Na SCSS wijzigingen: verwijder `assets/css_compiled/theme.css` en `.mtime` om te forceren
|
||||
|
||||
```bash
|
||||
rm themes/mijn-thema/assets/css_compiled/theme.css themes/mijn-thema/assets/css_compiled/.mtime
|
||||
```
|
||||
|
||||
## guide.twig
|
||||
|
||||
Speciale layout voor handleidingen. Injecteert de Navigation plugin in de sidebar voor zijbalk navigatie. Wordt automatisch gebruikt voor pagina's in de `guide/` map.
|
||||
|
||||
## Plugin CSS laden
|
||||
|
||||
Plugin CSS wordt automatisch geladen ná theme CSS in `base.twig`, zodat thema's plugin styling kunnen overschrijven:
|
||||
|
||||
```twig
|
||||
{% for cssUrl in plugin_css_urls|default([]) %}
|
||||
<link href="{{ cssUrl }}" rel="stylesheet">
|
||||
{% endfor %}
|
||||
├── scss/theme.scss # SCSS bron
|
||||
├── css/ # CSS bestanden
|
||||
├── js/theme.js # JavaScript
|
||||
├── fonts/ # Lettertypes
|
||||
└── img/ # Afbeeldingen
|
||||
```
|
||||
@@ -168,11 +168,6 @@ return [
|
||||
// Media
|
||||
'media' => 'Medien',
|
||||
'no_media' => 'Keine Mediendateien gefunden.',
|
||||
'media_insert' => 'Medien einfügen',
|
||||
'media_filter' => 'Nach Dateinamen filtern...',
|
||||
'media_loading' => 'Medien werden geladen...',
|
||||
'media_empty' => 'Keine Mediendateien in content/ gefunden. Laden Sie zuerst Dateien über Medien im Menü hoch.',
|
||||
'media_empty_plugin' => 'Keine Mediendateien im Assets-Ordner dieses Plugins gefunden. Platzieren Sie Dateien direkt im Ordner plugins/{plugin}/assets/, um sie hier einfügen zu können.',
|
||||
|
||||
// Plugins
|
||||
'new_plugin' => 'Neues Plugin',
|
||||
@@ -247,19 +242,6 @@ return [
|
||||
'no_users' => 'Keine Benutzer gefunden.',
|
||||
'new_user' => 'Neuer Benutzer',
|
||||
'add_user' => 'Benutzer hinzufügen',
|
||||
'users_list' => 'Benutzerliste',
|
||||
'search_users' => 'Nach Benutzername, E-Mail oder Autor-Name suchen...',
|
||||
'all_roles' => 'Alle Rollen',
|
||||
'filter' => 'Filtern',
|
||||
'delete' => 'Löschen',
|
||||
'back_to_users' => 'Zurück zu Benutzer',
|
||||
'profile_info' => 'Profilinformationen',
|
||||
'change_password' => 'Passwort ändern',
|
||||
'new_password' => 'Neues Passwort',
|
||||
'confirm_password' => 'Passwort bestätigen',
|
||||
'passwords_no_match' => 'Passwörter stimmen nicht überein.',
|
||||
'danger_zone' => 'Gefahrenzone',
|
||||
'delete_user' => 'Benutzer löschen',
|
||||
|
||||
// Login
|
||||
'login_title' => 'CodePress Admin - Anmeldung',
|
||||
@@ -267,15 +249,4 @@ return [
|
||||
'password_label' => 'Passwort',
|
||||
'login_btn' => 'Anmelden',
|
||||
'back_to_website' => 'Zurück zur Website',
|
||||
|
||||
// Plugin editor (plugins-edit)
|
||||
'plugin_files' => 'Plugin-Dateien',
|
||||
'plugin_no_files' => 'Keine Dateien gefunden.',
|
||||
'plugin_new_file' => 'Neue Datei',
|
||||
'plugin_new_file_title' => 'Neue Datei erstellen',
|
||||
'plugin_file_path' => 'Dateipfad innerhalb des Plugins',
|
||||
'plugin_file_path_help' => 'Nur Buchstaben, Zahlen, Punkte, Unterstriche, Bindestriche und Schrägstriche. Unterverzeichnisse werden automatisch erstellt.',
|
||||
'plugin_not_editable' => 'Dieser Dateityp kann im Editor nicht bearbeitet werden.',
|
||||
'plugin_select_file' => 'Wählen Sie eine Datei aus der Seitenleiste zum Bearbeiten.',
|
||||
'plugin_upload_help' => 'Dateien werden in den Ordner assets/ dieses Plugins hochgeladen. Erlaubt: Bilder, Video, Audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
];
|
||||
@@ -35,14 +35,4 @@ return [
|
||||
'plugin_development' => 'Plugin Entwicklung',
|
||||
'template_system' => 'Template System',
|
||||
'go_to' => 'Gehe zu',
|
||||
'welcome_title' => 'Willkommen bei CodePress CMS',
|
||||
'welcome_new_install' => 'Neue Installation',
|
||||
'welcome_intro' => 'Sie sehen diese Seite, weil das Inhaltsverzeichnis noch leer ist. Dies ist eine neue Installation von CodePress CMS.',
|
||||
'welcome_next_steps' => 'Nächste Schritte',
|
||||
'welcome_step_1' => 'Melden Sie sich über die Admin-Konsole an, um Inhalte zu verwalten',
|
||||
'welcome_step_2' => 'Fügen Sie Markdown-, PHP- oder HTML-Dateien zum Inhaltsverzeichnis hinzu',
|
||||
'welcome_step_3' => 'Lesen Sie das Handbuch für die vollständige Dokumentation',
|
||||
'welcome_admin_link' => 'Zur Admin-Konsole',
|
||||
'welcome_guide_link' => 'Handbuch ansehen',
|
||||
'welcome_content_empty' => 'Das Inhaltsverzeichnis ist leer',
|
||||
];
|
||||
@@ -168,11 +168,6 @@ return [
|
||||
// Media
|
||||
'media' => 'Media',
|
||||
'no_media' => 'No media files found.',
|
||||
'media_insert' => 'Insert media',
|
||||
'media_filter' => 'Filter by filename...',
|
||||
'media_loading' => 'Loading media...',
|
||||
'media_empty' => 'No media files found in content/. Upload files first via Media in the menu.',
|
||||
'media_empty_plugin' => 'No media files found in this plugin assets folder. Place files directly in plugins/{plugin}/assets/ to insert them here.',
|
||||
|
||||
// Plugins
|
||||
'new_plugin' => 'New plugin',
|
||||
@@ -247,19 +242,6 @@ return [
|
||||
'no_users' => 'No users found.',
|
||||
'new_user' => 'New user',
|
||||
'add_user' => 'Add user',
|
||||
'users_list' => 'User list',
|
||||
'search_users' => 'Search by username, email or author name...',
|
||||
'all_roles' => 'All roles',
|
||||
'filter' => 'Filter',
|
||||
'delete' => 'Delete',
|
||||
'back_to_users' => 'Back to users',
|
||||
'profile_info' => 'Profile information',
|
||||
'change_password' => 'Change password',
|
||||
'new_password' => 'New password',
|
||||
'confirm_password' => 'Confirm password',
|
||||
'passwords_no_match' => 'Passwords do not match.',
|
||||
'danger_zone' => 'Danger zone',
|
||||
'delete_user' => 'Delete user',
|
||||
|
||||
// Login
|
||||
'login_title' => 'CodePress Admin - Login',
|
||||
@@ -267,38 +249,4 @@ return [
|
||||
'password_label' => 'Password',
|
||||
'login_btn' => 'Log in',
|
||||
'back_to_website' => 'Back to website',
|
||||
|
||||
// Role switch (admin testing feature)
|
||||
'role_switch' => 'Switch role',
|
||||
'role_switch_title' => 'Switch role',
|
||||
'role_switch_help' => 'Test the admin from another role. Your real account stays admin.',
|
||||
'role_switch_btn' => 'Switch to role',
|
||||
'role_testing' => 'Testing',
|
||||
'role_reset_title' => 'Reset role',
|
||||
'role_reset_btn' => 'Back to admin',
|
||||
'role_reset_help' => 'Click below to return to your admin role.',
|
||||
'role_override_active' => 'You are currently testing as',
|
||||
'cannot_change_own_role' => 'You cannot change your own role.',
|
||||
'cannot_change_password_role' => 'Changing passwords is not allowed in this role.',
|
||||
'created' => 'Created',
|
||||
|
||||
// Plugin config
|
||||
'plugin_settings' => 'Settings',
|
||||
'plugin_no_settings' => 'This plugin has no configurable settings.',
|
||||
'plugin_info' => 'Plugin information',
|
||||
'plugin_name' => 'Name',
|
||||
'version' => 'Version',
|
||||
'type' => 'Type',
|
||||
'description' => 'Description',
|
||||
|
||||
// Plugin editor (plugins-edit)
|
||||
'plugin_files' => 'Plugin files',
|
||||
'plugin_no_files' => 'No files found.',
|
||||
'plugin_new_file' => 'New file',
|
||||
'plugin_new_file_title' => 'Create new file',
|
||||
'plugin_file_path' => 'File path within plugin',
|
||||
'plugin_file_path_help' => 'Only letters, digits, dots, underscores, hyphens and slashes. Subdirectories are created automatically.',
|
||||
'plugin_not_editable' => 'This file type cannot be edited in the editor.',
|
||||
'plugin_select_file' => 'Select a file from the sidebar to edit.',
|
||||
'plugin_upload_help' => 'Files are uploaded to assets/ of this plugin. Allowed: images, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
];
|
||||
@@ -35,14 +35,4 @@ return [
|
||||
'plugin_development' => 'Plugin Development',
|
||||
'template_system' => 'Template System',
|
||||
'go_to' => 'Go to',
|
||||
'welcome_title' => 'Welcome to CodePress CMS',
|
||||
'welcome_new_install' => 'New installation',
|
||||
'welcome_intro' => 'You are seeing this page because the content directory is still empty. This is a new installation of CodePress CMS.',
|
||||
'welcome_next_steps' => 'Next steps',
|
||||
'welcome_step_1' => 'Log in via the admin console to manage content',
|
||||
'welcome_step_2' => 'Add Markdown, PHP or HTML files to the content directory',
|
||||
'welcome_step_3' => 'Read the manual for full documentation',
|
||||
'welcome_admin_link' => 'Go to admin console',
|
||||
'welcome_guide_link' => 'View manual',
|
||||
'welcome_content_empty' => 'The content directory is empty',
|
||||
];
|
||||
@@ -168,11 +168,6 @@ return [
|
||||
// Media
|
||||
'media' => 'Media',
|
||||
'no_media' => 'Geen media bestanden gevonden.',
|
||||
'media_insert' => 'Media invoegen',
|
||||
'media_filter' => 'Filter op bestandsnaam...',
|
||||
'media_loading' => 'Media laden...',
|
||||
'media_empty' => 'Geen media bestanden gevonden in content/. Upload eerst bestanden via Media in het menu.',
|
||||
'media_empty_plugin' => 'Geen media bestanden gevonden in de assets-map van deze plugin. Plaats bestanden rechtstreeks in de map plugins/{plugin}/assets/ om ze hier te kunnen invoegen.',
|
||||
|
||||
// Plugins
|
||||
'new_plugin' => 'Nieuwe plugin',
|
||||
@@ -247,19 +242,6 @@ return [
|
||||
'no_users' => 'Geen gebruikers gevonden.',
|
||||
'new_user' => 'Nieuwe gebruiker',
|
||||
'add_user' => 'Gebruiker toevoegen',
|
||||
'users_list' => 'Gebruikerslijst',
|
||||
'search_users' => 'Zoek op gebruikersnaam, e-mail of auteur naam...',
|
||||
'all_roles' => 'Alle rollen',
|
||||
'filter' => 'Filteren',
|
||||
'delete' => 'Verwijderen',
|
||||
'back_to_users' => 'Terug naar gebruikers',
|
||||
'profile_info' => 'Profiel gegevens',
|
||||
'change_password' => 'Wachtwoord wijzigen',
|
||||
'new_password' => 'Nieuw wachtwoord',
|
||||
'confirm_password' => 'Bevestig wachtwoord',
|
||||
'passwords_no_match' => 'Wachtwoorden komen niet overeen.',
|
||||
'danger_zone' => 'Gevarenzone',
|
||||
'delete_user' => 'Gebruiker verwijderen',
|
||||
|
||||
// Login
|
||||
'login_title' => 'CodePress Admin - Login',
|
||||
@@ -267,38 +249,4 @@ return [
|
||||
'password_label' => 'Wachtwoord',
|
||||
'login_btn' => 'Inloggen',
|
||||
'back_to_website' => 'Terug naar website',
|
||||
|
||||
// Role switch (admin testing feature)
|
||||
'role_switch' => 'Rol wisselen',
|
||||
'role_switch_title' => 'Rol wisselen',
|
||||
'role_switch_help' => 'Test de admin vanuit een andere rol. Je echte account blijft admin.',
|
||||
'role_switch_btn' => 'Wissel naar rol',
|
||||
'role_testing' => 'Testen',
|
||||
'role_reset_title' => 'Rol terugzetten',
|
||||
'role_reset_btn' => 'Terug naar admin',
|
||||
'role_reset_help' => 'Klik hieronder om terug te keren naar je admin rol.',
|
||||
'role_override_active' => 'Je test momenteel als',
|
||||
'cannot_change_own_role' => 'Je kunt je eigen rol niet wijzigen.',
|
||||
'cannot_change_password_role' => 'Wachtwoord wijzigen is niet toegestaan in deze rol.',
|
||||
'created' => 'Aangemaakt',
|
||||
|
||||
// Plugin config
|
||||
'plugin_settings' => 'Instellingen',
|
||||
'plugin_no_settings' => 'Deze plugin heeft geen instelbare configuratie.',
|
||||
'plugin_info' => 'Plugin informatie',
|
||||
'plugin_name' => 'Naam',
|
||||
'version' => 'Versie',
|
||||
'type' => 'Type',
|
||||
'description' => 'Beschrijving',
|
||||
|
||||
// Plugin editor (plugins-edit)
|
||||
'plugin_files' => 'Plugin bestanden',
|
||||
'plugin_no_files' => 'Geen bestanden gevonden.',
|
||||
'plugin_new_file' => 'Nieuw bestand',
|
||||
'plugin_new_file_title' => 'Nieuw bestand aanmaken',
|
||||
'plugin_file_path' => 'Bestandspad binnen plugin',
|
||||
'plugin_file_path_help' => 'Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt.',
|
||||
'plugin_not_editable' => 'Dit bestandstype kan niet in de editor bewerkt worden.',
|
||||
'plugin_select_file' => 'Selecteer een bestand uit de zijbalk om te bewerken.',
|
||||
'plugin_upload_help' => 'Bestanden worden geüpload naar assets/ van deze plugin. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
];
|
||||
@@ -35,14 +35,4 @@ return [
|
||||
'plugin_development' => 'Plugin Ontwikkeling',
|
||||
'template_system' => 'Template Systeem',
|
||||
'go_to' => 'Ga naar',
|
||||
'welcome_title' => 'Welkom bij CodePress CMS',
|
||||
'welcome_new_install' => 'Nieuwe installatie',
|
||||
'welcome_intro' => 'U ziet deze pagina omdat de content-map nog leeg is. Dit is een nieuwe installatie van CodePress CMS.',
|
||||
'welcome_next_steps' => 'Volgende stappen',
|
||||
'welcome_step_1' => 'Log in via de admin console om content te beheren',
|
||||
'welcome_step_2' => 'Voeg Markdown-, PHP- of HTML-bestanden toe in de content-map',
|
||||
'welcome_step_3' => 'Bekijk de handleiding voor uitgebreide documentatie',
|
||||
'welcome_admin_link' => 'Naar de admin console',
|
||||
'welcome_guide_link' => 'Handleiding bekijken',
|
||||
'welcome_content_empty' => 'De content-map is leeg',
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "codepress",
|
||||
"version": "1.7.1",
|
||||
"description": "A lightweight, file-based Content Management System built with PHP and Bootstrap.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build:css": "npx sass --load-path=node_modules src/scss/main.scss public/assets/css/style.css --style=compressed",
|
||||
"watch:css": "npx sass --load-path=node_modules --watch src/scss/main.scss public/assets/css/style.css",
|
||||
"build": "npm run build:css",
|
||||
"clean": "rm -rf node_modules package-lock.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.noorlander.info/E.Noorlander/CodePress.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8",
|
||||
"sass": "^1.94.2"
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,8 @@ class Dashboard
|
||||
'plugin' => 'Dashboard',
|
||||
'route' => 'dashboard',
|
||||
'label' => 'Dashboard',
|
||||
'label_key' => 'menu_label',
|
||||
'icon' => 'bi-speedometer2',
|
||||
'section' => 'general',
|
||||
'permission' => 'dashboard',
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -41,12 +39,6 @@ class Dashboard
|
||||
$enabledPlugins = $this->api ? $this->api->getEnabledPlugins() : [];
|
||||
$versionInfo = $this->api ? $this->api->getVersionInfo() : ['version' => '0.0.0'];
|
||||
|
||||
// Plugin translations (admin context). Falls back to plugin default_language.
|
||||
$t = $this->api ? $this->api->getPluginTranslations('Dashboard') : [];
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
$stats = [
|
||||
'pages' => $this->countFiles($contentDir, ['md', 'php', 'html']),
|
||||
'directories' => $this->countDirs($contentDir),
|
||||
@@ -76,20 +68,20 @@ class Dashboard
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> <?= htmlspecialchars($tr('page_title')) ?></h2>
|
||||
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><i class="bi bi-info-circle"></i> <?= htmlspecialchars($tr('site_info')) ?></div>
|
||||
<div class="card-header"><i class="bi bi-info-circle"></i> Site informatie</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('site_title')) ?></td><td><?= htmlspecialchars($siteTitle) ?></td></tr>
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('default_lang')) ?></td><td><?= htmlspecialchars($defaultLang) ?></td></tr>
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('cms_version')) ?></td><td><?= htmlspecialchars($stats['cms_version']) ?></td></tr>
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('php_version')) ?></td><td><?= htmlspecialchars($stats['php_version']) ?></td></tr>
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('os')) ?></td><td><?= htmlspecialchars($stats['os']) ?></td></tr>
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('config_loaded')) ?></td><td><?php if ($stats['config_exists']): ?><span class="badge bg-success"><?= htmlspecialchars($tr('yes')) ?></span><?php else: ?><span class="badge bg-danger"><?= htmlspecialchars($tr('no')) ?></span><?php endif; ?></td></tr>
|
||||
<tr><td class="text-muted">Site titel</td><td><?= htmlspecialchars($siteTitle) ?></td></tr>
|
||||
<tr><td class="text-muted">Standaard taal</td><td><?= htmlspecialchars($defaultLang) ?></td></tr>
|
||||
<tr><td class="text-muted">CodePress versie</td><td><?= htmlspecialchars($stats['cms_version']) ?></td></tr>
|
||||
<tr><td class="text-muted">PHP versie</td><td><?= htmlspecialchars($stats['php_version']) ?></td></tr>
|
||||
<tr><td class="text-muted">Besturingssysteem</td><td><?= htmlspecialchars($stats['os']) ?></td></tr>
|
||||
<tr><td class="text-muted">Config geladen</td><td><?php if ($stats['config_exists']): ?><span class="badge bg-success">Ja</span><?php else: ?><span class="badge bg-danger">Nee</span><?php endif; ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,12 +89,12 @@ class Dashboard
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><i class="bi bi-folder2-open"></i> <?= htmlspecialchars($tr('content_info')) ?></div>
|
||||
<div class="card-header"><i class="bi bi-folder2-open"></i> Content informatie</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('pages')) ?></td><td><span class="badge bg-primary"><?= $stats['pages'] ?></span></td></tr>
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('folders')) ?></td><td><span class="badge bg-warning text-dark"><?= $stats['directories'] ?></span></td></tr>
|
||||
<tr><td class="text-muted"><?= htmlspecialchars($tr('content_size')) ?></td><td><?= htmlspecialchars($stats['content_size']) ?></td></tr>
|
||||
<tr><td class="text-muted">Pagina's</td><td><span class="badge bg-primary"><?= $stats['pages'] ?></span></td></tr>
|
||||
<tr><td class="text-muted">Mappen</td><td><span class="badge bg-warning text-dark"><?= $stats['directories'] ?></span></td></tr>
|
||||
<tr><td class="text-muted">Content grootte</td><td><?= htmlspecialchars($stats['content_size']) ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,8 +103,8 @@ class Dashboard
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-plug"></i> <?= htmlspecialchars($tr('plugins')) ?></span>
|
||||
<a href="/admin/plugins" class="btn btn-sm btn-outline-secondary"><?= htmlspecialchars($tr('manage')) ?></a>
|
||||
<span><i class="bi bi-plug"></i> Plugins</span>
|
||||
<a href="/admin/plugins" class="btn btn-sm btn-outline-secondary">Beheren</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
@@ -121,22 +113,22 @@ class Dashboard
|
||||
<td>
|
||||
<i class="bi bi-plug-fill"></i> <?= htmlspecialchars($pluginName) ?>
|
||||
<?php if ($info['type'] === 'system'): ?>
|
||||
<span class="badge bg-secondary fs-7"><?= htmlspecialchars($tr('plugin_type_system')) ?></span>
|
||||
<span class="badge bg-secondary fs-7">systeem</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-info fs-7"><?= htmlspecialchars($tr('plugin_type_content')) ?></span>
|
||||
<span class="badge bg-info fs-7">content</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($info['enabled']): ?>
|
||||
<span class="badge bg-success"><?= htmlspecialchars($tr('active')) ?></span>
|
||||
<span class="badge bg-success">Actief</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary"><?= htmlspecialchars($tr('inactive')) ?></span>
|
||||
<span class="badge bg-secondary">Inactief</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($pluginOverview)): ?>
|
||||
<tr><td colspan="2" class="text-muted text-center"><?= htmlspecialchars($tr('no_plugins')) ?></td></tr>
|
||||
<tr><td colspan="2" class="text-muted text-center">Geen plugins gevonden.</td></tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Dashboard Plugin
|
||||
|
||||
Toont site informatie, content statistieken en een plugin overzicht op het admin dashboard.
|
||||
|
||||
## Plugin type
|
||||
|
||||
**Systeem** plugin. De plugin-output volgt de geselecteerde **admin taal** (via `AdminPluginAPI::getPluginTranslations()` met `admin.php`).
|
||||
|
||||
## Bestandsstructuur
|
||||
|
||||
```
|
||||
Dashboard/
|
||||
├── Dashboard.php # Hoofd plugin class
|
||||
├── plugin.json # Plugin metadata + instellingen
|
||||
├── README.md # Dit bestand
|
||||
├── assets/ # Optionele CSS/JS (leeg)
|
||||
└── language/ # Vertalingen
|
||||
├── nl/admin.php # Nederlandse admin labels
|
||||
└── en/admin.php # Engelse admin labels
|
||||
```
|
||||
|
||||
## Functies
|
||||
|
||||
- **Site informatie**: Site titel, standaard taal, CodePress/PHP versie, OS, config status
|
||||
- **Content informatie**: Aantal pagina's, mappen, content grootte
|
||||
- **Plugin overzicht**: Lijst van alle plugins met type (systeem/content) en actief/inactief status
|
||||
|
||||
## Instellingen
|
||||
|
||||
Deze plugin heeft geen instelbare configuratie (`hasConfig: false`).
|
||||
|
||||
## Taal support
|
||||
|
||||
De plugin gebruikt `AdminPluginAPI::getPluginTranslations('Dashboard')` om labels op te halen. Fallback chain:
|
||||
1. Geselecteerde admin taal
|
||||
2. Plugin `default_language` (`nl`)
|
||||
3. Lege array (sleutel wordt ongewijzigd getoond)
|
||||
|
||||
Beschikbare sleutels staan in `language/<lang>/admin.php`. Het admin-menu label wordt vertaald via `label_key: 'menu_label'` in `getAdminMenu()`.
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'Dashboard',
|
||||
|
||||
// Page header
|
||||
'page_title' => 'Dashboard',
|
||||
|
||||
// Site information card
|
||||
'site_info' => 'Site information',
|
||||
'site_title' => 'Site title',
|
||||
'default_lang' => 'Default language',
|
||||
'cms_version' => 'CodePress version',
|
||||
'php_version' => 'PHP version',
|
||||
'os' => 'Operating system',
|
||||
'config_loaded' => 'Config loaded',
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
|
||||
// Content information card
|
||||
'content_info' => 'Content information',
|
||||
'pages' => 'Pages',
|
||||
'folders' => 'Folders',
|
||||
'content_size' => 'Content size',
|
||||
|
||||
// Plugins card
|
||||
'plugins' => 'Plugins',
|
||||
'manage' => 'Manage',
|
||||
'plugin_type_system' => 'system',
|
||||
'plugin_type_content' => 'content',
|
||||
'active' => 'Active',
|
||||
'inactive' => 'Inactive',
|
||||
'no_plugins' => 'No plugins found.',
|
||||
];
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'Dashboard',
|
||||
|
||||
// Page header
|
||||
'page_title' => 'Dashboard',
|
||||
|
||||
// Site informatie card
|
||||
'site_info' => 'Site informatie',
|
||||
'site_title' => 'Site titel',
|
||||
'default_lang' => 'Standaard taal',
|
||||
'cms_version' => 'CodePress versie',
|
||||
'php_version' => 'PHP versie',
|
||||
'os' => 'Besturingssysteem',
|
||||
'config_loaded' => 'Config geladen',
|
||||
'yes' => 'Ja',
|
||||
'no' => 'Nee',
|
||||
|
||||
// Content informatie card
|
||||
'content_info' => 'Content informatie',
|
||||
'pages' => "Pagina's",
|
||||
'folders' => 'Mappen',
|
||||
'content_size' => 'Content grootte',
|
||||
|
||||
// Plugins card
|
||||
'plugins' => 'Plugins',
|
||||
'manage' => 'Beheren',
|
||||
'plugin_type_system' => 'systeem',
|
||||
'plugin_type_content' => 'content',
|
||||
'active' => 'Actief',
|
||||
'inactive' => 'Inactief',
|
||||
'no_plugins' => 'Geen plugins gevonden.',
|
||||
];
|
||||
@@ -5,7 +5,5 @@
|
||||
"description": "Toont site informatie, content statistieken en plugin overzicht op het dashboard.",
|
||||
"type": "system",
|
||||
"essential": true,
|
||||
"hasConfig": false,
|
||||
"default_language": "nl",
|
||||
"settings": []
|
||||
"hasConfig": false
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
class GeoIPInfo
|
||||
{
|
||||
private ?PluginAPIInterface $api = null;
|
||||
|
||||
public function setAPI(PluginAPIInterface $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'GeoIP Info',
|
||||
'type' => 'system',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin menu items.
|
||||
*/
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'plugin' => 'GeoIPInfo',
|
||||
'label' => 'GeoIP Info',
|
||||
'label_key' => 'menu_label',
|
||||
'route' => 'geoip-info',
|
||||
'icon' => 'bi-globe2',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin routes this plugin handles.
|
||||
*/
|
||||
public function getAdminRoutes(): array
|
||||
{
|
||||
return ['geoip-info'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle admin route — render the admin page.
|
||||
*/
|
||||
public function handleAdminRoute(string $route): void
|
||||
{
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
$geoip = new GeoIP();
|
||||
$countryCode = $geoip->lookupCountry($ip);
|
||||
$country = GeoIP::getCountryName($countryCode);
|
||||
$flag = GeoIP::getCountryFlagEmoji($countryCode);
|
||||
|
||||
$t = $this->api ? $this->api->getPluginTranslations('GeoIPInfo') : [];
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
echo '<h2 class="mb-4"><i class="bi bi-globe2"></i> ' . htmlspecialchars($tr('page_title')) . '</h2>';
|
||||
echo '<div class="card shadow-sm"><div class="card-body">';
|
||||
echo '<table class="table table-sm">';
|
||||
echo '<tr><td>' . htmlspecialchars($tr('ip_address')) . '</td><td><code>' . htmlspecialchars($ip) . '</code></td></tr>';
|
||||
echo '<tr><td>' . htmlspecialchars($tr('country')) . '</td><td>' . $flag . ' ' . htmlspecialchars($country) . '</td></tr>';
|
||||
echo '</table>';
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
# GeoIPInfo Plugin
|
||||
|
||||
Systeem plugin die GeoIP-informatie (IP adres en land) toont op een admin-pagina.
|
||||
|
||||
## Plugin type
|
||||
|
||||
**Systeem** plugin. De plugin-output volgt de geselecteerde **admin taal** (via `AdminPluginAPI::getPluginTranslations()` met `admin.php`).
|
||||
|
||||
## Bestandsstructuur
|
||||
|
||||
```
|
||||
GeoIPInfo/
|
||||
├── GeoIPInfo.php # Hoofd plugin class
|
||||
├── plugin.json # Plugin metadata + instellingen
|
||||
├── README.md # Dit bestand
|
||||
├── assets/ # Optionele CSS/JS (leeg)
|
||||
└── language/ # Vertalingen
|
||||
├── nl/admin.php # Nederlandse admin labels
|
||||
└── en/admin.php # Engelse admin labels
|
||||
```
|
||||
|
||||
## Functies
|
||||
|
||||
- Toont het IP adres van de bezoeker
|
||||
- Toont het land (met vlag-emoji) gebaseerd op GeoIP lookup
|
||||
|
||||
## Instellingen
|
||||
|
||||
Deze plugin heeft geen instelbare configuratie (`hasConfig: false`).
|
||||
|
||||
## Taal support
|
||||
|
||||
De plugin gebruikt `AdminPluginAPI::getPluginTranslations('GeoIPInfo')` om labels op te halen. Fallback chain:
|
||||
1. Geselecteerde admin taal
|
||||
2. Plugin `default_language` (`nl`)
|
||||
3. Lege array (sleutel wordt ongewijzigd getoond)
|
||||
|
||||
Beschikbare sleutels staan in `language/<lang>/admin.php`. Het admin-menu label wordt vertaald via `label_key: 'menu_label'` in `getAdminMenu()`.
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'GeoIP Info',
|
||||
|
||||
// Page
|
||||
'page_title' => 'GeoIP Info',
|
||||
'ip_address' => 'IP address',
|
||||
'country' => 'Country',
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'GeoIP Info',
|
||||
|
||||
// Page
|
||||
'page_title' => 'GeoIP Info',
|
||||
'ip_address' => 'IP adres',
|
||||
'country' => 'Land',
|
||||
];
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "GeoIPInfo",
|
||||
"version": "1.0.0",
|
||||
"author": "CodePress",
|
||||
"description": "Systeem plugin voor GeoIP informatie in admin",
|
||||
"type": "system",
|
||||
"essential": false,
|
||||
"hasConfig": false,
|
||||
"default_language": "nl",
|
||||
"settings": []
|
||||
}
|
||||
@@ -20,22 +20,14 @@ class HTMLBlock
|
||||
|
||||
public function getSidebarContent(): string
|
||||
{
|
||||
// Content plugin: translations follow the current content language.
|
||||
$t = $this->api ? $this->api->getPluginTranslations('HTMLBlock') : [];
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
$currentPage = $this->api ? $this->api->getCurrentPageTitle() : $tr('unknown');
|
||||
$currentPage = $this->api ? $this->api->getCurrentPageTitle() : 'Onbekend';
|
||||
$isHomepage = $this->api ? $this->api->isHomepage() : false;
|
||||
$currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl';
|
||||
$currentPath = $_GET['page'] ?? '';
|
||||
$currentPath = preg_replace('/\.(md|php|html)$/', '', $currentPath);
|
||||
|
||||
$content = '
|
||||
<p class="mb-2"><strong>' . htmlspecialchars($tr('current_page')) . ':</strong> ' . htmlspecialchars($currentPage) . '</p>
|
||||
<p class="mb-2"><strong>' . htmlspecialchars($tr('language')) . ':</strong> ' . strtoupper($currentLang) . '</p>
|
||||
<p class="mb-3"><strong>' . htmlspecialchars($tr('homepage')) . ':</strong> ' . htmlspecialchars($isHomepage ? $tr('yes') : $tr('no')) . '</p>';
|
||||
<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>';
|
||||
|
||||
// Add page-specific content
|
||||
if ($this->api) {
|
||||
@@ -44,41 +36,24 @@ class HTMLBlock
|
||||
$content .= '
|
||||
<div class="alert alert-info mb-3">
|
||||
<small>
|
||||
<strong>' . htmlspecialchars($tr('file_info')) . ':</strong><br>
|
||||
' . htmlspecialchars($tr('created')) . ': ' . htmlspecialchars($fileInfo['created']) . '<br>
|
||||
' . htmlspecialchars($tr('modified')) . ': ' . htmlspecialchars($fileInfo['modified']) . '
|
||||
<strong>Bestandsinfo:</strong><br>
|
||||
Aangemaakt: ' . htmlspecialchars($fileInfo['created']) . '<br>
|
||||
Gewijzigd: ' . htmlspecialchars($fileInfo['modified']) . '
|
||||
</small>
|
||||
</div>';
|
||||
}
|
||||
|
||||
// Add dynamic quick navigation
|
||||
// Add quick navigation
|
||||
$menu = $this->api->getMenu();
|
||||
if (!empty($menu)) {
|
||||
$content .= '
|
||||
<h6>' . htmlspecialchars($tr('quick_navigation')) . '</h6>
|
||||
<h6>Quick Navigation</h6>
|
||||
<ul class="list-unstyled mb-3">';
|
||||
|
||||
foreach ($menu as $item) {
|
||||
if ($item['type'] === 'file') {
|
||||
$url = '/' . $currentLang . '/' . $item['path'];
|
||||
$isActive = ($item['path'] === $currentPath) ? ' fw-bold' : '';
|
||||
$content .= '<li><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $isActive . '"><i class="bi bi-file-earmark"></i> ' . htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
|
||||
} elseif ($item['type'] === 'folder' && !empty($item['children'])) {
|
||||
$url = '/' . $currentLang . '/' . $item['path'];
|
||||
$isActive = strpos($currentPath, $item['path']) === 0 ? ' fw-bold' : '';
|
||||
$content .= '<li><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $isActive . '"><i class="bi bi-folder"></i> ' . htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
|
||||
// Show children if current page is in this folder
|
||||
if (strpos($currentPath, $item['path']) === 0) {
|
||||
$content .= '<ul class="list-unstyled ms-3 mb-1">';
|
||||
foreach ($item['children'] as $child) {
|
||||
if ($child['type'] === 'file') {
|
||||
$childUrl = '/' . $currentLang . '/' . $child['path'];
|
||||
$childActive = ($child['path'] === $currentPath) ? ' fw-bold' : '';
|
||||
$content .= '<li><a href="' . htmlspecialchars($childUrl, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $childActive . '"><i class="bi bi-file-earmark"></i> ' . htmlspecialchars($child['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
|
||||
}
|
||||
}
|
||||
$content .= '</ul>';
|
||||
}
|
||||
$url = $this->api->createUrl($item['path']);
|
||||
$content .= '<li><a href="' . htmlspecialchars($url) . '" class="text-decoration-none">📄 ' . htmlspecialchars($item['title']) . '</a></li>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
-27
@@ -1,39 +1,100 @@
|
||||
# HTMLBlock Plugin
|
||||
|
||||
Toont een custom HTML-blok in de sidebar van content-pagina's met pagina-informatie en quick navigation.
|
||||
|
||||
## Plugin type
|
||||
|
||||
**Content** plugin. De plugin-output volgt de geselecteerde **content taal** (via `CMSAPI::getPluginTranslations()` met `site.php`).
|
||||
|
||||
## Bestandsstructuur
|
||||
|
||||
```
|
||||
HTMLBlock/
|
||||
├── HTMLBlock.php # Hoofd plugin class
|
||||
├── plugin.json # Plugin metadata + instellingen
|
||||
├── README.md # Dit bestand
|
||||
├── assets/ # Optionele CSS/JS (leeg)
|
||||
└── language/ # Vertalingen
|
||||
├── nl/site.php # Nederlandse front-end labels
|
||||
└── en/site.php # Engelse front-end labels
|
||||
```
|
||||
Deze plugin toont een custom HTML blok in de sidebar met pagina-informatie en navigatie.
|
||||
|
||||
## Functies
|
||||
|
||||
- **Pagina informatie**: Toont huidige pagina titel en metadata
|
||||
- **Bestandsinfo**: Aanmaak- en wijzigingsdatums
|
||||
- **Quick Navigation**: Genereert quick links uit het CMS menu
|
||||
- **Dynamische navigatie**: Genereert quick links uit het menu
|
||||
- **Interactive controls**: Verversen en sidebar toggle
|
||||
- **Responsive**: Werkt op desktop en mobiel
|
||||
|
||||
## Instellingen
|
||||
## Installatie
|
||||
|
||||
Deze plugin heeft geen instelbare configuratie (`hasConfig: false`).
|
||||
1. Kopieer de `HTMLBlock` map naar `plugins/`
|
||||
2. De plugin wordt automatisch geladen
|
||||
|
||||
## Taal support
|
||||
## Gebruik
|
||||
|
||||
De plugin gebruikt `CMSAPI::getPluginTranslations('HTMLBlock')` om labels op te halen. Fallback chain:
|
||||
1. Geselecteerde content taal
|
||||
2. Plugin `default_language` (`nl`)
|
||||
3. Lege array (sleutel wordt ongewijzigd getoond)
|
||||
De plugin wordt automatisch in de sidebar geladen en toont:
|
||||
|
||||
Beschikbare sleutels staan in `language/<lang>/site.php`.
|
||||
### Huidige Pagina Info
|
||||
- Pagina titel
|
||||
- Huidige taal
|
||||
- Homepage status
|
||||
|
||||
### Bestandsinformatie
|
||||
- Aanmaakdatum
|
||||
- Laatste wijziging
|
||||
- Bestandsgrootte
|
||||
|
||||
### Quick Navigation
|
||||
- Dynamische links uit het CMS menu
|
||||
- Automatische URL generatie
|
||||
|
||||
### Interactive Controls
|
||||
- **Ververs Content**: Herlaadt de huidige pagina
|
||||
- **Toggle Sidebar**: Toont/verbergt de sidebar
|
||||
|
||||
## Customization
|
||||
|
||||
De plugin content kan worden aangepast door de `getSidebarContent()` methode te wijzigen in `HTMLBlock.php`.
|
||||
|
||||
### Voorbeeld Custom Content
|
||||
|
||||
```php
|
||||
public function getSidebarContent(): string
|
||||
{
|
||||
$currentPage = $this->api ? $this->api->getCurrentPageTitle() : 'Onbekend';
|
||||
|
||||
return '
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5>Mijn Custom Block</h5>
|
||||
<p>Huidige pagina: ' . htmlspecialchars($currentPage) . '</p>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
De plugin maakt gebruik van de CMS API voor:
|
||||
|
||||
- `getCurrentPageTitle()` - Huidige pagina titel
|
||||
- `getCurrentLanguage()` - Huidige taal
|
||||
- `isHomepage()` - Check of homepage
|
||||
- `getCurrentPageFileInfo()` - Bestandsinformatie
|
||||
- `getMenu()` - Menu structuur
|
||||
- `createUrl($page, $lang)` - URL generatie
|
||||
|
||||
## Styling
|
||||
|
||||
De plugin gebruikt Bootstrap 5 classes:
|
||||
- `card`, `card-header`, `card-body` voor kaarten
|
||||
- `btn`, `btn-outline-primary` voor knoppen
|
||||
- `list-unstyled` voor navigatie
|
||||
|
||||
## JavaScript
|
||||
|
||||
De plugin bevat JavaScript voor:
|
||||
- Pagina verversen
|
||||
- Sidebar toggle functionaliteit
|
||||
- Dynamische content updates
|
||||
|
||||
## Development
|
||||
|
||||
De plugin is een goed voorbeeld voor:
|
||||
- API integratie
|
||||
- Dynamic content generatie
|
||||
- User interface components
|
||||
- Responsive design
|
||||
|
||||
## Bestandsstructuur
|
||||
|
||||
```
|
||||
HTMLBlock/
|
||||
├── HTMLBlock.php # Hoofd plugin bestand
|
||||
└── README.md # Deze documentatie
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Sidebar card
|
||||
'title' => 'HTML Block',
|
||||
'current_page' => 'Current page',
|
||||
'language' => 'Language',
|
||||
'homepage' => 'Homepage',
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
'unknown' => 'Unknown',
|
||||
'file_info' => 'File info',
|
||||
'created' => 'Created',
|
||||
'modified' => 'Modified',
|
||||
'quick_navigation' => 'Quick Navigation',
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Sidebar card
|
||||
'title' => 'HTML Block',
|
||||
'current_page' => 'Huidige pagina',
|
||||
'language' => 'Taal',
|
||||
'homepage' => 'Homepage',
|
||||
'yes' => 'Ja',
|
||||
'no' => 'Nee',
|
||||
'unknown' => 'Onbekend',
|
||||
'file_info' => 'Bestandsinfo',
|
||||
'created' => 'Aangemaakt',
|
||||
'modified' => 'Gewijzigd',
|
||||
'quick_navigation' => 'Quick Navigation',
|
||||
];
|
||||
@@ -5,7 +5,5 @@
|
||||
"description": "Toont aangepaste HTML-blokken in de sidebar van content-pagina's.",
|
||||
"type": "content",
|
||||
"essential": false,
|
||||
"hasConfig": false,
|
||||
"default_language": "nl",
|
||||
"settings": []
|
||||
"hasConfig": false
|
||||
}
|
||||
+9
-59
@@ -20,63 +20,19 @@ class Logs
|
||||
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
$requiredRoles = $config['required_roles'] ?? ['bi-manager', 'site-admin', 'admin'];
|
||||
|
||||
return [
|
||||
[
|
||||
'plugin' => 'Logs',
|
||||
'route' => 'logs',
|
||||
'label' => 'Logs',
|
||||
'label_key' => 'menu_label',
|
||||
'icon' => 'bi-journal-text',
|
||||
'section' => 'general',
|
||||
'permission' => 'logs',
|
||||
'required_roles' => $requiredRoles,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this plugin's runtime config (defaults + overrides).
|
||||
*/
|
||||
private function getPluginConfig(): array
|
||||
{
|
||||
$pluginDir = dirname(__DIR__);
|
||||
$pluginJsonFile = $pluginDir . '/plugin.json';
|
||||
$configJsonFile = $pluginDir . '/config.json';
|
||||
|
||||
$defaults = [];
|
||||
if (file_exists($pluginJsonFile)) {
|
||||
$pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? [];
|
||||
foreach ($pluginJson['settings'] ?? [] as $setting) {
|
||||
if (isset($setting['key'])) {
|
||||
$defaults[$setting['key']] = $setting['default'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$overrides = [];
|
||||
if (file_exists($configJsonFile)) {
|
||||
$overrides = json_decode(file_get_contents($configJsonFile), true) ?? [];
|
||||
}
|
||||
|
||||
return array_merge($defaults, $overrides);
|
||||
}
|
||||
|
||||
public function handleAdminRoute(string $action): ?string
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
$maxLines = (int)($config['max_lines'] ?? 100);
|
||||
$showAdminTab = $config['show_admin_tab'] ?? true;
|
||||
$showRequestsTab = $config['show_requests_tab'] ?? true;
|
||||
|
||||
// System plugin: translations resolve against the admin language.
|
||||
$t = $this->api ? $this->api->getPluginTranslations('Logs') : [];
|
||||
$tr = function (string $key) use ($t): string {
|
||||
return $t[$key] ?? $key;
|
||||
};
|
||||
|
||||
$tab = $_GET['tab'] ?? 'admin';
|
||||
$logDir = dirname(__DIR__, 2) . '/admin/storage/logs';
|
||||
$logFile = $tab === 'requests' ? $logDir . '/requests.log' : $logDir . '/admin.log';
|
||||
@@ -84,7 +40,7 @@ class Logs
|
||||
$logs = [];
|
||||
if (file_exists($logFile)) {
|
||||
$lines = file($logFile) ?: [];
|
||||
$lines = array_slice($lines, -$maxLines);
|
||||
$lines = array_slice($lines, -100);
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/', trim($line), $m)) {
|
||||
$logs[] = [
|
||||
@@ -100,37 +56,31 @@ class Logs
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<h2 class="mb-4"><i class="bi bi-journal-text"></i> <?= htmlspecialchars($tr('page_title')) ?></h2>
|
||||
<h2 class="mb-4"><i class="bi bi-journal-text"></i> Logs</h2>
|
||||
|
||||
<?php if ($showAdminTab || $showRequestsTab): ?>
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<?php if ($showAdminTab): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $tab === 'admin' ? 'active' : '' ?>" href="/admin/logs?tab=admin"><?= htmlspecialchars($tr('tab_admin')) ?></a>
|
||||
<a class="nav-link <?= $tab === 'admin' ? 'active' : '' ?>" href="/admin/logs?tab=admin">Admin</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if ($showRequestsTab): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $tab === 'requests' ? 'active' : '' ?>" href="/admin/logs?tab=requests"><?= htmlspecialchars($tr('tab_requests')) ?></a>
|
||||
<a class="nav-link <?= $tab === 'requests' ? 'active' : '' ?>" href="/admin/logs?tab=requests">Requests</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= htmlspecialchars($tr('col_time')) ?></th>
|
||||
<th><?= htmlspecialchars($tr('col_level')) ?></th>
|
||||
<th><?= htmlspecialchars($tr('col_ip')) ?></th>
|
||||
<th><?= htmlspecialchars($tr('col_message')) ?></th>
|
||||
<th>Tijd</th>
|
||||
<th>Level</th>
|
||||
<th>IP</th>
|
||||
<th>Bericht</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($logs)): ?>
|
||||
<tr><td colspan="4" class="text-muted text-center py-4"><?= htmlspecialchars($tr('no_logs')) ?></td></tr>
|
||||
<tr><td colspan="4" class="text-muted text-center py-4">Geen logs gevonden.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($logs as $log): ?>
|
||||
<tr>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# Logs Plugin
|
||||
|
||||
Toont admin activiteitlogs en front-end request logs met filter tabs op een admin-pagina.
|
||||
|
||||
## Plugin type
|
||||
|
||||
**Systeem** plugin. De plugin-output volgt de geselecteerde **admin taal** (via `AdminPluginAPI::getPluginTranslations()` met `admin.php`).
|
||||
|
||||
## Bestandsstructuur
|
||||
|
||||
```
|
||||
Logs/
|
||||
├── Logs.php # Hoofd plugin class
|
||||
├── plugin.json # Plugin metadata + instellingen
|
||||
├── README.md # Dit bestand
|
||||
├── assets/ # Optionele CSS/JS (leeg)
|
||||
└── language/ # Vertalingen
|
||||
├── nl/admin.php # Nederlandse admin labels
|
||||
└── en/admin.php # Engelse admin labels
|
||||
```
|
||||
|
||||
## Functies
|
||||
|
||||
- **Admin tab**: Toont admin activiteitlogs (uit `admin/storage/logs/admin.log`)
|
||||
- **Requests tab**: Toont front-end request logs (uit `admin/storage/logs/requests.log`)
|
||||
- Tabbladen kunnen via de instellingen in/uitgeschakeld worden
|
||||
- Aantal getoonde regels is instelbaar (meest recente eerst)
|
||||
|
||||
## Instellingen
|
||||
|
||||
Deze plugin heeft instelbare configuratie (`hasConfig: true`). Instellingen worden gedefinieerd in `plugin.json` onder `settings` en overschreven via `config.json`.
|
||||
|
||||
| Sleutel | Type | Standaard | Beschrijving |
|
||||
|---------|------|-----------|--------------|
|
||||
| `required_roles` | multi-select | `bi-manager, site-admin, admin` | Rollen die deze plugin mogen zien |
|
||||
| `max_lines` | number | `100` | Aantal logregels dat getoond wordt (meest recente eerst) |
|
||||
| `show_admin_tab` | checkbox | `true` | Schakel de admin activiteit-log tab in of uit |
|
||||
| `show_requests_tab` | checkbox | `true` | Schakel de front-end request-log tab in of uit |
|
||||
|
||||
De label- en help-teksten voor instellingen worden vertaald via `label_key`/`help_key` (verwijst naar sleutels in `language/<lang>/admin.php`).
|
||||
|
||||
## Taal support
|
||||
|
||||
De plugin gebruikt `AdminPluginAPI::getPluginTranslations('Logs')` om labels op te halen. Fallback chain:
|
||||
1. Geselecteerde admin taal
|
||||
2. Plugin `default_language` (`nl`)
|
||||
3. Lege array (sleutel wordt ongewijzigd getoond)
|
||||
|
||||
Beschikbare sleutels staan in `language/<lang>/admin.php`. Het admin-menu label wordt vertaald via `label_key: 'menu_label'` in `getAdminMenu()`.
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'Logs',
|
||||
|
||||
// Page
|
||||
'page_title' => 'Logs',
|
||||
'tab_admin' => 'Admin',
|
||||
'tab_requests' => 'Requests',
|
||||
'col_time' => 'Time',
|
||||
'col_level' => 'Level',
|
||||
'col_ip' => 'IP',
|
||||
'col_message' => 'Message',
|
||||
'no_logs' => 'No logs found.',
|
||||
'analytics_disabled' => 'Analytics is disabled.',
|
||||
|
||||
// Settings (plugin.json label_key/help_key)
|
||||
'setting_required_roles' => 'Roles allowed to view this plugin',
|
||||
'setting_required_roles_help' => 'Determines which roles can access the logs page in the admin panel. Admin always has access.',
|
||||
'role_options' => [
|
||||
'admin' => 'Admin',
|
||||
'content-manager' => 'Content Manager',
|
||||
'bi-manager' => 'BI Manager',
|
||||
'site-admin' => 'Site Admin',
|
||||
],
|
||||
'setting_max_lines' => 'Maximum number of lines',
|
||||
'setting_max_lines_help' => 'Number of log lines shown (most recent first).',
|
||||
'setting_show_admin_tab' => 'Show admin log tab',
|
||||
'setting_show_admin_tab_help' => 'Enable or disable the admin activity log tab.',
|
||||
'setting_show_requests_tab' => 'Show request log tab',
|
||||
'setting_show_requests_tab_help' => 'Enable or disable the front-end request log tab.',
|
||||
];
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
// Menu
|
||||
'menu_label' => 'Logs',
|
||||
|
||||
// Page
|
||||
'page_title' => 'Logs',
|
||||
'tab_admin' => 'Admin',
|
||||
'tab_requests' => 'Requests',
|
||||
'col_time' => 'Tijd',
|
||||
'col_level' => 'Level',
|
||||
'col_ip' => 'IP',
|
||||
'col_message' => 'Bericht',
|
||||
'no_logs' => 'Geen logs gevonden.',
|
||||
'analytics_disabled' => 'Analytics is uitgeschakeld.',
|
||||
|
||||
// Settings (plugin.json label_key/help_key)
|
||||
'setting_required_roles' => 'Rollen die deze plugin mogen zien',
|
||||
'setting_required_roles_help' => 'Bepaalt welke rollen toegang hebben tot de logs-pagina in het admin-paneel. Admin heeft altijd toegang.',
|
||||
'role_options' => [
|
||||
'admin' => 'Admin',
|
||||
'content-manager' => 'Content Beheerder',
|
||||
'bi-manager' => 'BI Beheerder',
|
||||
'site-admin' => 'Site Admin',
|
||||
],
|
||||
'setting_max_lines' => 'Maximaal aantal regels',
|
||||
'setting_max_lines_help' => 'Aantal logregels dat getoond wordt (meest recente eerst).',
|
||||
'setting_show_admin_tab' => 'Toon admin-log tab',
|
||||
'setting_show_admin_tab_help' => 'Schakel de admin activiteit-log tab in of uit.',
|
||||
'setting_show_requests_tab' => 'Toon request-log tab',
|
||||
'setting_show_requests_tab_help' => 'Schakel de front-end request-log tab in of uit.',
|
||||
];
|
||||
@@ -5,43 +5,5 @@
|
||||
"description": "Toont admin activiteitlogs en front-end request logs met filter tabs.",
|
||||
"type": "system",
|
||||
"essential": false,
|
||||
"hasConfig": true,
|
||||
"default_language": "nl",
|
||||
"settings": [
|
||||
{
|
||||
"key": "required_roles",
|
||||
"label_key": "setting_required_roles",
|
||||
"help_key": "setting_required_roles_help",
|
||||
"option_label_key": "role_options",
|
||||
"type": "multi-select",
|
||||
"default": ["bi-manager", "site-admin", "admin"],
|
||||
"options": {
|
||||
"admin": "Admin",
|
||||
"content-manager": "Content Beheerder",
|
||||
"bi-manager": "BI Beheerder",
|
||||
"site-admin": "Site Admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "max_lines",
|
||||
"label_key": "setting_max_lines",
|
||||
"help_key": "setting_max_lines_help",
|
||||
"type": "number",
|
||||
"default": 100
|
||||
},
|
||||
{
|
||||
"key": "show_admin_tab",
|
||||
"label_key": "setting_show_admin_tab",
|
||||
"help_key": "setting_show_admin_tab_help",
|
||||
"type": "checkbox",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "show_requests_tab",
|
||||
"label_key": "setting_show_requests_tab",
|
||||
"help_key": "setting_show_requests_tab_help",
|
||||
"type": "checkbox",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
"hasConfig": true
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
# Navigation Plugin
|
||||
|
||||
Essentiële content plugin die navigatie toont voor handleidingen en content in de sidebar.
|
||||
|
||||
## Plugin type
|
||||
|
||||
**Content** plugin. De plugin-output volgt de geselecteerde **content taal** (via `CMSAPI::getPluginTranslations()` met `site.php`).
|
||||
|
||||
## Bestandsstructuur
|
||||
|
||||
```
|
||||
Navigation/
|
||||
├── Navigation.php # Hoofd plugin class
|
||||
├── plugin.json # Plugin metadata + instellingen
|
||||
├── README.md # Dit bestand
|
||||
├── assets/ # CSS/SCSS voor navigatie
|
||||
│ ├── css/navigation.css
|
||||
│ └── scss/navigation.scss
|
||||
└── language/ # Vertalingen
|
||||
├── nl/site.php # Nederlandse front-end labels
|
||||
└── en/site.php # Engelse front-end labels
|
||||
```
|
||||
|
||||
## Functies
|
||||
|
||||
- **Content navigatie**: Bouwt een sidebar navigatie uit de content-mapstructuur
|
||||
- **Handleiding navigatie**: Bouwt een sidebar navigatie uit de guide-mapstructuur
|
||||
- Detecteert automatisch of de huidige pagina een handleiding of content pagina is
|
||||
- Toont mappen (met kinderen) en bestanden (.md, .php, .html)
|
||||
- Strip taal-prefixen uit bestandsnamen (bijv. `nl.pagina.md` → `Pagina`)
|
||||
|
||||
## Instellingen
|
||||
|
||||
Deze plugin heeft geen instelbare configuratie (`hasConfig: false`). Wel levert hij een CSS-URL via `getCssUrl()`.
|
||||
|
||||
## Taal support
|
||||
|
||||
De plugin-output bestaat voornamelijk uit bestands- en mapnamen (die uit de content zelf komen), dus er zijn weinig te vertalen UI-strings. De `language/<lang>/site.php` bestanden bevatten momenteel alleen `plugin_title` voor uniformiteit met de andere plugins en toekomstige uitbreiding.
|
||||
|
||||
De plugin gebruikt `CMSAPI::getPluginTranslations('Navigation')` om labels op te halen. Fallback chain:
|
||||
1. Geselecteerde content taal
|
||||
2. Plugin `default_language` (`nl`)
|
||||
3. Lege array (sleutel wordt ongewijzigd getoond)
|
||||
|
||||
## Beschermd
|
||||
|
||||
Deze plugin is **protected**: hardcoded in `public/admin.php` (`getProtectedPlugins()`) en kan niet worden uitgeschakeld, verwijderd of bewerkt via de admin interface.
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
// Navigation is a content plugin. Its sidebar output is built from file and
|
||||
// directory names, so there are very few translatable UI strings. This file
|
||||
// exists for uniformity with the other plugins and to allow future labels.
|
||||
return [
|
||||
'plugin_title' => 'Navigation',
|
||||
];
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
// Navigation is a content plugin. Its sidebar output is built from file and
|
||||
// directory names, so there are very few translatable UI strings. This file
|
||||
// exists for uniformity with the other plugins and to allow future labels.
|
||||
return [
|
||||
'plugin_title' => 'Navigatie',
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user