Initial dev export (exclude uploads/runtime)
This commit is contained in:
218
modules/media/MediaController.php
Normal file
218
modules/media/MediaController.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Media;
|
||||
|
||||
use Core\Http\Response;
|
||||
use Core\Services\Auth;
|
||||
use Core\Services\Database;
|
||||
use Core\Views\View;
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
class MediaController
|
||||
{
|
||||
private View $view;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->view = new View(__DIR__ . '/views');
|
||||
}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return new Response('', 302, ['Location' => '/admin/login']);
|
||||
}
|
||||
if (!Auth::hasRole(['admin', 'manager'])) {
|
||||
return new Response('', 302, ['Location' => '/admin']);
|
||||
}
|
||||
|
||||
$db = Database::get();
|
||||
$items = [];
|
||||
$folders = [];
|
||||
$folderId = isset($_GET['folder']) ? (int)$_GET['folder'] : 0;
|
||||
if ($db instanceof PDO) {
|
||||
$folderStmt = $db->query("SELECT id, name FROM ac_media_folders ORDER BY name ASC");
|
||||
$folders = $folderStmt ? $folderStmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
if ($folderId > 0) {
|
||||
$stmt = $db->prepare("SELECT id, file_name, file_url, file_type, file_size, created_at FROM ac_media WHERE folder_id = :folder_id ORDER BY created_at DESC");
|
||||
$stmt->execute([':folder_id' => $folderId]);
|
||||
} else {
|
||||
$stmt = $db->query("SELECT id, file_name, file_url, file_type, file_size, created_at FROM ac_media WHERE folder_id IS NULL ORDER BY created_at DESC");
|
||||
}
|
||||
$items = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
}
|
||||
|
||||
$error = (string)($_GET['error'] ?? '');
|
||||
$success = (string)($_GET['success'] ?? '');
|
||||
return new Response($this->view->render('admin/index.php', [
|
||||
'title' => 'Media',
|
||||
'items' => $items,
|
||||
'folders' => $folders,
|
||||
'active_folder' => $folderId,
|
||||
'error' => $error,
|
||||
'success' => $success,
|
||||
]));
|
||||
}
|
||||
|
||||
public function picker(): Response
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return new Response('', 302, ['Location' => '/admin/login']);
|
||||
}
|
||||
if (!Auth::hasRole(['admin', 'manager', 'editor'])) {
|
||||
return new Response('', 302, ['Location' => '/admin']);
|
||||
}
|
||||
|
||||
$db = Database::get();
|
||||
$items = [];
|
||||
if ($db instanceof PDO) {
|
||||
$stmt = $db->query("SELECT id, file_name, file_url, file_type FROM ac_media ORDER BY created_at DESC");
|
||||
$items = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
}
|
||||
|
||||
return new Response(json_encode(['items' => $items]), 200, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
|
||||
public function upload(): Response
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return new Response('', 302, ['Location' => '/admin/login']);
|
||||
}
|
||||
if (!Auth::hasRole(['admin', 'manager'])) {
|
||||
return new Response('', 302, ['Location' => '/admin']);
|
||||
}
|
||||
|
||||
$file = $_FILES['media_file'] ?? null;
|
||||
$folderId = isset($_POST['folder_id']) ? (int)$_POST['folder_id'] : 0;
|
||||
if (!$file || !isset($file['tmp_name'])) {
|
||||
return $this->uploadError('No file uploaded.', $folderId);
|
||||
}
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
return $this->uploadError($this->uploadErrorMessage((int)$file['error']), $folderId);
|
||||
}
|
||||
|
||||
$tmp = (string)$file['tmp_name'];
|
||||
if ($tmp === '' || !is_uploaded_file($tmp)) {
|
||||
return new Response('', 302, ['Location' => '/admin/media']);
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
|
||||
if ($ext === '') {
|
||||
$ext = 'bin';
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../../uploads/media';
|
||||
if (!is_dir($uploadDir)) {
|
||||
if (!mkdir($uploadDir, 0755, true)) {
|
||||
return $this->uploadError('Upload directory could not be created.', $folderId);
|
||||
}
|
||||
}
|
||||
if (!is_writable($uploadDir)) {
|
||||
return $this->uploadError('Upload directory is not writable.', $folderId);
|
||||
}
|
||||
|
||||
$baseName = preg_replace('~[^a-z0-9]+~', '-', strtolower((string)$file['name'])) ?? 'file';
|
||||
$baseName = trim($baseName, '-');
|
||||
$fileName = ($baseName !== '' ? $baseName : 'file') . '-' . date('YmdHis') . '.' . $ext;
|
||||
$dest = $uploadDir . '/' . $fileName;
|
||||
if (!move_uploaded_file($tmp, $dest)) {
|
||||
return $this->uploadError('Upload failed. Check server permissions.', $folderId);
|
||||
}
|
||||
|
||||
$fileUrl = '/uploads/media/' . $fileName;
|
||||
$fileType = (string)($file['type'] ?? '');
|
||||
$fileSize = (int)($file['size'] ?? 0);
|
||||
|
||||
$db = Database::get();
|
||||
if ($db instanceof PDO) {
|
||||
try {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO ac_media (file_name, file_url, file_type, file_size, folder_id)
|
||||
VALUES (:name, :url, :type, :size, :folder_id)
|
||||
");
|
||||
$stmt->execute([
|
||||
':name' => (string)$file['name'],
|
||||
':url' => $fileUrl,
|
||||
':type' => $fileType,
|
||||
':size' => $fileSize,
|
||||
':folder_id' => $folderId > 0 ? $folderId : null,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return $this->uploadError('Database insert failed.', $folderId);
|
||||
}
|
||||
}
|
||||
|
||||
$redirect = $folderId > 0 ? '/admin/media?folder=' . $folderId . '&success=1' : '/admin/media?success=1';
|
||||
return new Response('', 302, ['Location' => $redirect]);
|
||||
}
|
||||
|
||||
public function delete(): Response
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return new Response('', 302, ['Location' => '/admin/login']);
|
||||
}
|
||||
if (!Auth::hasRole(['admin', 'manager'])) {
|
||||
return new Response('', 302, ['Location' => '/admin']);
|
||||
}
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$db = Database::get();
|
||||
if ($db instanceof PDO && $id > 0) {
|
||||
$stmt = $db->prepare("SELECT file_url FROM ac_media WHERE id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row && !empty($row['file_url'])) {
|
||||
$path = __DIR__ . '/../../..' . (string)$row['file_url'];
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
$db->prepare("DELETE FROM ac_media WHERE id = :id")->execute([':id' => $id]);
|
||||
}
|
||||
return new Response('', 302, ['Location' => '/admin/media']);
|
||||
}
|
||||
|
||||
public function createFolder(): Response
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return new Response('', 302, ['Location' => '/admin/login']);
|
||||
}
|
||||
if (!Auth::hasRole(['admin', 'manager'])) {
|
||||
return new Response('', 302, ['Location' => '/admin']);
|
||||
}
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
return new Response('', 302, ['Location' => '/admin/media']);
|
||||
}
|
||||
$db = Database::get();
|
||||
if ($db instanceof PDO) {
|
||||
$stmt = $db->prepare("INSERT INTO ac_media_folders (name) VALUES (:name)");
|
||||
$stmt->execute([':name' => $name]);
|
||||
}
|
||||
return new Response('', 302, ['Location' => '/admin/media']);
|
||||
}
|
||||
|
||||
private function uploadError(string $message, int $folderId): Response
|
||||
{
|
||||
$target = $folderId > 0 ? '/admin/media?folder=' . $folderId : '/admin/media';
|
||||
$target .= '&error=' . rawurlencode($message);
|
||||
return new Response('', 302, ['Location' => $target]);
|
||||
}
|
||||
|
||||
private function uploadErrorMessage(int $code): string
|
||||
{
|
||||
$max = (string)ini_get('upload_max_filesize');
|
||||
$map = [
|
||||
UPLOAD_ERR_INI_SIZE => "File exceeds upload_max_filesize ({$max}).",
|
||||
UPLOAD_ERR_FORM_SIZE => 'File exceeds form limit.',
|
||||
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded.',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'Missing temp upload directory.',
|
||||
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
|
||||
UPLOAD_ERR_EXTENSION => 'Upload stopped by a PHP extension.',
|
||||
UPLOAD_ERR_NO_FILE => 'No file uploaded.',
|
||||
];
|
||||
return $map[$code] ?? 'Upload failed.';
|
||||
}
|
||||
}
|
||||
16
modules/media/module.php
Normal file
16
modules/media/module.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Http\Router;
|
||||
use Modules\Media\MediaController;
|
||||
|
||||
require_once __DIR__ . '/MediaController.php';
|
||||
|
||||
return function (Router $router): void {
|
||||
$controller = new MediaController();
|
||||
$router->get('/admin/media', [$controller, 'index']);
|
||||
$router->get('/admin/media/picker', [$controller, 'picker']);
|
||||
$router->post('/admin/media/upload', [$controller, 'upload']);
|
||||
$router->post('/admin/media/delete', [$controller, 'delete']);
|
||||
$router->post('/admin/media/folders', [$controller, 'createFolder']);
|
||||
};
|
||||
114
modules/media/views/admin/index.php
Normal file
114
modules/media/views/admin/index.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
$pageTitle = 'Media';
|
||||
$items = $items ?? [];
|
||||
$folders = $folders ?? [];
|
||||
$activeFolder = (int)($active_folder ?? 0);
|
||||
$error = (string)($error ?? '');
|
||||
$success = (string)($success ?? '');
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card">
|
||||
<div class="badge">Media</div>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
|
||||
<div>
|
||||
<h1 style="font-size:28px; margin:0;">Media Library</h1>
|
||||
<p style="color: var(--muted); margin-top:6px;">Upload and reuse images across pages, posts, and newsletters.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div style="margin-top:12px; color:#f3b0b0; font-size:13px;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php elseif ($success !== ''): ?>
|
||||
<div style="margin-top:12px; color:var(--accent-2); font-size:13px;">Upload complete.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top:18px; display:grid; gap:12px;">
|
||||
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<a href="/admin/media" class="btn outline small" style="<?= $activeFolder === 0 ? 'border-color: var(--accent); color: var(--text);' : '' ?>">All</a>
|
||||
<?php foreach ($folders as $folder): ?>
|
||||
<a href="/admin/media?folder=<?= (int)$folder['id'] ?>" class="btn outline small" style="<?= $activeFolder === (int)$folder['id'] ? 'border-color: var(--accent); color: var(--text);' : '' ?>">
|
||||
<?= htmlspecialchars((string)$folder['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/media/folders" style="display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<input class="input" name="name" placeholder="New folder name" style="max-width:280px;">
|
||||
<button type="submit" class="btn outline small">Create Folder</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/admin/media/upload" enctype="multipart/form-data" id="mediaUploadForm">
|
||||
<input type="hidden" name="folder_id" value="<?= $activeFolder > 0 ? $activeFolder : 0 ?>">
|
||||
<label for="mediaFileInput" id="mediaDropzone" style="display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; padding:24px; border-radius:16px; border:1px dashed rgba(255,255,255,0.2); background: rgba(0,0,0,0.2); cursor:pointer;">
|
||||
<div style="font-size:12px; text-transform:uppercase; letter-spacing:0.2em; color:var(--muted);">Drag & Drop</div>
|
||||
<div style="font-size:14px; color:var(--text);">or click to upload</div>
|
||||
<div id="mediaFileName" style="font-size:12px; color:var(--muted);">No file selected</div>
|
||||
</label>
|
||||
<input class="input" type="file" id="mediaFileInput" name="media_file" accept="image/*" style="display:none;">
|
||||
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
|
||||
<button type="submit" class="btn small">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:18px; display:grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap:12px;">
|
||||
<?php if (!$items): ?>
|
||||
<div style="color: var(--muted); font-size:13px;">No media uploaded yet.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<div style="border-radius:16px; border:1px solid var(--stroke); background: rgba(14,14,16,0.9); padding:10px;">
|
||||
<div style="aspect-ratio: 1 / 1; border-radius:12px; overflow:hidden; background:#0b0c10; display:flex; align-items:center; justify-content:center;">
|
||||
<?php if (str_starts_with((string)($item['file_type'] ?? ''), 'image/')): ?>
|
||||
<img src="<?= htmlspecialchars((string)$item['file_url'], ENT_QUOTES, 'UTF-8') ?>" alt="" style="width:100%; height:100%; object-fit:cover;">
|
||||
<?php else: ?>
|
||||
<div style="color:var(--muted); font-size:12px;">File</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="margin-top:8px; font-size:12px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
|
||||
<?= htmlspecialchars((string)($item['file_name'] ?? ''), ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
<input class="input" readonly value="<?= htmlspecialchars((string)($item['file_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" style="margin-top:6px; font-size:11px;">
|
||||
<form method="post" action="/admin/media/delete" onsubmit="return confirm('Delete this file?');" style="margin-top:8px;">
|
||||
<input type="hidden" name="id" value="<?= (int)($item['id'] ?? 0) ?>">
|
||||
<button type="submit" class="btn outline small">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function () {
|
||||
const dropzone = document.getElementById('mediaDropzone');
|
||||
const fileInput = document.getElementById('mediaFileInput');
|
||||
const fileName = document.getElementById('mediaFileName');
|
||||
if (!dropzone || !fileInput || !fileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
dropzone.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
dropzone.style.borderColor = 'var(--accent)';
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragleave', () => {
|
||||
dropzone.style.borderColor = 'rgba(255,255,255,0.2)';
|
||||
});
|
||||
|
||||
dropzone.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
dropzone.style.borderColor = 'rgba(255,255,255,0.2)';
|
||||
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length) {
|
||||
fileInput.files = event.dataTransfer.files;
|
||||
fileName.textContent = event.dataTransfer.files[0].name;
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
fileName.textContent = fileInput.files.length ? fileInput.files[0].name : 'No file selected';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../admin/views/layout.php';
|
||||
Reference in New Issue
Block a user