197 lines
6.0 KiB
PHP
197 lines
6.0 KiB
PHP
<?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');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|