seedproject-web/api/public/controllers/adminprojects.php

114 lines
4.4 KiB
PHP
Raw Normal View History

<?php
use App\Controllers\PublicController;
/**
* Structured editing of projects for the admin console (galleries, Instagram,
* cover). Content lives in cja_projects; the static site reads it at build
* time, so a save is followed by a rebuild (the console's /devconsole/rebuild).
*
* GET /api/adminprojects/list -> [{slug,title,galleryCount,...}]
* GET /api/adminprojects/get?slug=... -> full editable record
* POST /api/adminprojects/save -> { slug, gallery, links, cover_image }
*
* All admin-gated. Only media-related fields are writable here; the freeform
* agent console handles prose and layout.
*/
class AdminProjects extends PublicController
{
public function list(): void
{
$this->requireAdmin();
$rows = \Db::select('SELECT slug, title, status, gallery, links
FROM cja_projects ORDER BY sort_order ASC');
$out = array_map(function ($r) {
$gallery = json_decode($r['gallery'] ?? '[]', true) ?: [];
$links = json_decode($r['links'] ?? '{}', true) ?: [];
$filled = count(array_filter($gallery, fn($g) => !empty($g['src'])));
return [
'slug' => $r['slug'],
'title' => $r['title'],
'status' => $r['status'],
'gallery' => count($gallery),
'galleryFilled' => $filled,
'hasInstagram' => !empty($links['instagram']),
];
}, $rows);
$this->json(['projects' => $out]);
}
public function get(): void
{
$this->requireAdmin();
$slug = (string) ($_GET['slug'] ?? '');
$r = \Db::getRow('SELECT slug, title, cover_image, cover_alt, gallery, links
FROM cja_projects WHERE slug = ?', [$slug]);
if (!$r) {
$this->json(null, 404, ['code' => 'not_found', 'message' => 'No such project.']);
}
$this->json([
'slug' => $r['slug'],
'title' => $r['title'],
'cover' => $r['cover_image'] ?? '',
'coverAlt'=> $r['cover_alt'] ?? '',
'gallery' => json_decode($r['gallery'] ?? '[]', true) ?: [],
'links' => json_decode($r['links'] ?? '{}', true) ?: [],
]);
}
public function save(): void
{
$this->requireAdmin();
$this->guardPublic('adminprojects_save', 60, 300);
$in = json_decode((string) file_get_contents('php://input'), true) ?: [];
$slug = (string) ($in['slug'] ?? '');
$id = \Db::getValue('SELECT project_id FROM cja_projects WHERE slug = ?', [$slug]);
if (!$id) {
$this->json(null, 404, ['code' => 'not_found', 'message' => 'No such project.']);
}
// ---- gallery: array of {src, alt, caption} -------------------------
$gallery = [];
foreach ((array) ($in['gallery'] ?? []) as $g) {
$src = $this->safeMedia((string) ($g['src'] ?? ''));
$gallery[] = [
'src' => $src,
'alt' => mb_substr(trim((string) ($g['alt'] ?? '')), 0, 200),
'caption' => mb_substr(trim((string) ($g['caption'] ?? '')), 0, 120),
];
}
// ---- links: only known keys, only real URLs ------------------------
$links = [];
foreach (['live', 'instagram', 'github'] as $k) {
$v = trim((string) ($in['links'][$k] ?? ''));
if ($v !== '' && filter_var($v, FILTER_VALIDATE_URL)) {
$links[$k] = $v;
}
}
$cover = $this->safeMedia((string) ($in['cover_image'] ?? ''));
\Db::update('cja_projects', [
'gallery' => json_encode($gallery, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
'links' => json_encode($links, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
'cover_image' => $cover,
], 'project_id = ?', [$id]);
$this->json(['ok' => true, 'slug' => $slug]);
}
/**
* Only allow local media paths (uploads live under /media/ or /projects/),
* never arbitrary or off-site URLs the src is rendered into the page.
*/
private function safeMedia(string $s): string
{
$s = trim($s);
if ($s === '') return '';
return preg_match('#^/(media|projects|portfolio)/[\w./-]+$#', $s) ? $s : '';
}
}