Files
TripLog/src/Controllers/AttachmentController.php
T

197 lines
6.0 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Auth;
use App\Database;
class AttachmentController
{
private const ALLOWED_MIMES = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'image/heic' => 'heic',
'application/pdf' => 'pdf',
'application/msword' => 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.ms-excel' => 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/zip' => 'zip',
'video/mp4' => 'mp4',
'video/quicktime' => 'mov',
];
private const MAX_SIZE = 20 * 1024 * 1024;
public function node(): void
{
$this->requireLogin();
$nodeId = (int)($_GET['id'] ?? 0);
$node = $this->fetchNodeWithAccess($nodeId);
$db = Database::get();
$stmt = $db->prepare('SELECT a.*, u.name AS uploader_name
FROM attachments a
LEFT JOIN users u ON u.id = a.uploader_id
WHERE a.target_type = "trip_node" AND a.target_id = ?
ORDER BY a.created_at DESC');
$stmt->execute([$nodeId]);
$attachments = $stmt->fetchAll();
view('nodes/attachments', [
'node' => $node,
'attachments' => $attachments,
'pageTitle' => '节点附件:' . ($node['title'] ?: '无标题'),
]);
}
public function upload(): void
{
$this->requireLogin();
csrf_check();
$nodeId = (int)($_POST['node_id'] ?? 0);
$this->fetchNodeWithAccess($nodeId);
if (empty($_FILES['files']) || !is_array($_FILES['files']['name'])) {
flash('error', '未选择文件');
redirect('attachments/node', ['id' => $nodeId]);
}
$files = $this->restructureFiles($_FILES['files']);
$uploaded = 0;
$errors = [];
$db = Database::get();
$uploadDir = dirname(__DIR__, 2) . '/uploads/attachments';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
foreach ($files as $file) {
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = $file['name'] . ': 上传错误';
continue;
}
if ($file['size'] > self::MAX_SIZE) {
$errors[] = $file['name'] . ': 超过 20MB';
continue;
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!isset(self::ALLOWED_MIMES[$mime])) {
$errors[] = $file['name'] . ': 不支持的类型';
continue;
}
$ext = self::ALLOWED_MIMES[$mime];
$filename = date('Ymd') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$dest = $uploadDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
$errors[] = $file['name'] . ': 保存失败';
continue;
}
$stmt = $db->prepare('INSERT INTO attachments
(target_type, target_id, uploader_id, file_url, file_name)
VALUES ("trip_node", ?, ?, ?, ?)');
$stmt->execute([
$nodeId,
Auth::id(),
'/uploads/attachments/' . $filename,
$file['name'],
]);
$uploaded++;
}
$msg = "成功上传 {$uploaded} 个文件";
if (!empty($errors)) {
$msg .= ';失败:' . implode(',', $errors);
}
flash('error', $msg);
redirect('attachments/node', ['id' => $nodeId]);
}
public function destroy(): void
{
$this->requireLogin();
csrf_check();
$id = (int)($_POST['id'] ?? 0);
$db = Database::get();
$stmt = $db->prepare('SELECT * FROM attachments WHERE id = ? AND target_type = "trip_node"');
$stmt->execute([$id]);
$att = $stmt->fetch();
if (!$att) {
flash('error', '附件不存在');
redirect('activities/index');
}
$this->fetchNodeWithAccess((int)$att['target_id']);
$url = $att['file_url'];
if (strpos($url, '/uploads/attachments/') === 0) {
$file = dirname(__DIR__, 2) . $url;
if (is_file($file)) @unlink($file);
}
$stmt = $db->prepare('DELETE FROM attachments WHERE id = ?');
$stmt->execute([$id]);
flash('error', '附件已删除');
redirect('attachments/node', ['id' => (int)$att['target_id']]);
}
private function restructureFiles(array $files): array
{
$result = [];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
if (empty($files['name'][$i])) continue;
$result[] = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i],
];
}
return $result;
}
private function fetchNodeWithAccess(int $nodeId): array
{
$db = Database::get();
$stmt = $db->prepare('
SELECT tn.*, a.family_id, a.id AS activity_id, a.title AS activity_title
FROM trip_nodes tn
JOIN activities a ON a.id = tn.activity_id
WHERE tn.id = ?
');
$stmt->execute([$nodeId]);
$node = $stmt->fetch();
if (!$node) {
http_response_code(404);
exit('节点不存在');
}
if ((int)$node['family_id'] !== Auth::familyId()) {
http_response_code(403);
exit('无权访问此节点');
}
return $node;
}
private function requireLogin(): void
{
if (!Auth::check()) {
redirect('auth/login');
}
}
}