Комментарии, файлы и права проекта
- Система комментариев к задачам с вложенными ответами - Редактирование и удаление комментариев - Прикрепление файлов к задачам и комментариям (картинки, архивы до 10 МБ) - Система прав проекта: админ проекта может удалять чужие комментарии и файлы - Универсальный класс FileUpload для загрузки файлов - Защита загрузки: только автор комментария может добавлять файлы - Каскадное удаление: задача → комментарии → файлы - Автообновление комментариев в реальном времени
This commit is contained in:
246
backend/app/class/enity/class_comment.php
Normal file
246
backend/app/class/enity/class_comment.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
class Comment extends BaseEntity {
|
||||
|
||||
protected $db_name = 'comments';
|
||||
|
||||
// Свойства комментария
|
||||
public $id;
|
||||
public $id_task;
|
||||
public $id_accounts;
|
||||
public $id_answer; // ID родительского комментария (ответ на)
|
||||
public $text;
|
||||
public $date_create;
|
||||
|
||||
// Создание комментария
|
||||
public function create() {
|
||||
static::$error_message = [];
|
||||
|
||||
// Валидация
|
||||
if (!$this->id_task) {
|
||||
$this->addError('id_task', 'Задача не указана');
|
||||
}
|
||||
if (!$this->id_accounts) {
|
||||
$this->addError('id_accounts', 'Пользователь не указан');
|
||||
}
|
||||
if (!$this->text || trim($this->text) === '') {
|
||||
$this->addError('text', 'Текст комментария не может быть пустым');
|
||||
}
|
||||
if ($errors = $this->getErrors()) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
// Проверяем что задача существует
|
||||
Task::check_task($this->id_task);
|
||||
|
||||
// Если это ответ — проверяем что родительский комментарий существует
|
||||
if ($this->id_answer) {
|
||||
self::checkComment($this->id_answer);
|
||||
}
|
||||
|
||||
// Вставляем в базу
|
||||
Database::insert($this->db_name, [
|
||||
'id_task' => $this->id_task,
|
||||
'id_accounts' => $this->id_accounts,
|
||||
'id_answer' => $this->id_answer ?: null,
|
||||
'text' => $this->text,
|
||||
'date_create' => date('Y-m-d H:i:s'),
|
||||
'file_img' => '[]'
|
||||
]);
|
||||
|
||||
$this->id = Database::id();
|
||||
|
||||
// Возвращаем созданный комментарий с данными пользователя
|
||||
return [
|
||||
'success' => true,
|
||||
'comment' => $this->getById($this->id)
|
||||
];
|
||||
}
|
||||
|
||||
// Обновление комментария
|
||||
public function update() {
|
||||
static::$error_message = [];
|
||||
|
||||
// Валидация
|
||||
if (!$this->id) {
|
||||
$this->addError('id', 'ID комментария не указан');
|
||||
}
|
||||
if (!$this->text || trim($this->text) === '') {
|
||||
$this->addError('text', 'Текст комментария не может быть пустым');
|
||||
}
|
||||
if ($errors = $this->getErrors()) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
// Проверяем что комментарий существует
|
||||
$comment = self::checkComment($this->id);
|
||||
|
||||
// Проверяем что пользователь — автор комментария
|
||||
if ((int)$comment['id_accounts'] !== (int)$this->id_accounts) {
|
||||
$this->addError('access', 'Вы можете редактировать только свои комментарии');
|
||||
return $this->getErrors();
|
||||
}
|
||||
|
||||
// Обновляем в БД
|
||||
Database::update($this->db_name, [
|
||||
'text' => $this->text
|
||||
], [
|
||||
'id' => $this->id
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'comment' => $this->getById($this->id)
|
||||
];
|
||||
}
|
||||
|
||||
// Удаление комментария (с дочерними)
|
||||
public static function delete($id, $id_accounts) {
|
||||
// Проверяем что комментарий существует
|
||||
$comment = self::checkComment($id);
|
||||
|
||||
// Получаем задачу для проверки админа проекта
|
||||
$task = Database::get('cards_task', ['id_project'], ['id' => $comment['id_task']]);
|
||||
|
||||
// Проверяем права: автор комментария ИЛИ админ проекта
|
||||
$isAuthor = (int)$comment['id_accounts'] === (int)$id_accounts;
|
||||
$isProjectAdmin = $task && Project::isAdmin($task['id_project'], $id_accounts);
|
||||
|
||||
if (!$isAuthor && !$isProjectAdmin) {
|
||||
RestApi::response([
|
||||
'success' => false,
|
||||
'errors' => ['access' => 'Нет прав на удаление комментария']
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Рекурсивно удаляем все дочерние комментарии
|
||||
self::deleteWithChildren($id);
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
// Рекурсивное удаление комментария и всех его дочерних
|
||||
private static function deleteWithChildren($id) {
|
||||
// Находим все дочерние комментарии
|
||||
$children = Database::select('comments', ['id'], ['id_answer' => $id]);
|
||||
|
||||
// Рекурсивно удаляем дочерние
|
||||
if ($children) {
|
||||
foreach ($children as $child) {
|
||||
self::deleteWithChildren($child['id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем папку с файлами комментария
|
||||
FileUpload::deleteFolder('comment', $id);
|
||||
|
||||
// Удаляем сам комментарий
|
||||
Database::delete('comments', ['id' => $id]);
|
||||
}
|
||||
|
||||
// === МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ ===
|
||||
|
||||
// Загрузка файла к комментарию (только автор может загружать)
|
||||
public static function uploadFile($comment_id, $file_base64, $file_name, $user_id) {
|
||||
// Проверка что комментарий существует
|
||||
$comment = self::checkComment($comment_id);
|
||||
|
||||
// Проверка что пользователь — автор комментария
|
||||
if ((int)$comment['id_accounts'] !== (int)$user_id) {
|
||||
RestApi::response([
|
||||
'success' => false,
|
||||
'errors' => ['access' => 'Вы можете загружать файлы только к своим комментариям']
|
||||
], 403);
|
||||
}
|
||||
|
||||
return FileUpload::upload('comment', $comment_id, $file_base64, $file_name);
|
||||
}
|
||||
|
||||
// Удаление файлов комментария (автор или админ проекта)
|
||||
public static function deleteFile($comment_id, $file_names, $user_id) {
|
||||
// Проверка что комментарий существует
|
||||
$comment = self::checkComment($comment_id);
|
||||
|
||||
// Получаем задачу для проверки админа проекта
|
||||
$task = Database::get('cards_task', ['id_project'], ['id' => $comment['id_task']]);
|
||||
|
||||
// Проверка прав: автор комментария ИЛИ админ проекта
|
||||
$isAuthor = (int)$comment['id_accounts'] === (int)$user_id;
|
||||
$isProjectAdmin = $task && Project::isAdmin($task['id_project'], $user_id);
|
||||
|
||||
if (!$isAuthor && !$isProjectAdmin) {
|
||||
RestApi::response([
|
||||
'success' => false,
|
||||
'errors' => ['access' => 'Нет прав на удаление файлов']
|
||||
], 403);
|
||||
}
|
||||
|
||||
return FileUpload::delete('comment', $comment_id, $file_names);
|
||||
}
|
||||
|
||||
// Получение комментария по ID (с данными пользователя)
|
||||
public function getById($id) {
|
||||
$comment = Database::get($this->db_name, [
|
||||
'[>]accounts' => ['id_accounts' => 'id']
|
||||
], [
|
||||
'comments.id',
|
||||
'comments.id_task',
|
||||
'comments.id_accounts',
|
||||
'comments.id_answer',
|
||||
'comments.text',
|
||||
'comments.date_create',
|
||||
'comments.file_img',
|
||||
'accounts.name(author_name)',
|
||||
'accounts.avatar_url(author_avatar)'
|
||||
], [
|
||||
'comments.id' => $id
|
||||
]);
|
||||
|
||||
// Декодируем JSON файлов
|
||||
if ($comment) {
|
||||
$comment['file_img'] = $comment['file_img'] ? json_decode($comment['file_img'], true) : [];
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
// Получение всех комментариев задачи
|
||||
public function getByTask($id_task) {
|
||||
// Проверяем что задача существует
|
||||
Task::check_task($id_task);
|
||||
|
||||
$comments = Database::select($this->db_name, [
|
||||
'[>]accounts' => ['id_accounts' => 'id']
|
||||
], [
|
||||
'comments.id',
|
||||
'comments.id_task',
|
||||
'comments.id_accounts',
|
||||
'comments.id_answer',
|
||||
'comments.text',
|
||||
'comments.date_create',
|
||||
'comments.file_img',
|
||||
'accounts.name(author_name)',
|
||||
'accounts.avatar_url(author_avatar)'
|
||||
], [
|
||||
'comments.id_task' => $id_task,
|
||||
'ORDER' => ['comments.date_create' => 'ASC']
|
||||
]);
|
||||
|
||||
// Декодируем JSON файлов для каждого комментария
|
||||
return array_map(function($comment) {
|
||||
$comment['file_img'] = $comment['file_img'] ? json_decode($comment['file_img'], true) : [];
|
||||
return $comment;
|
||||
}, $comments ?: []);
|
||||
}
|
||||
|
||||
// Проверка и получение комментария (при ошибке — сразу ответ и exit)
|
||||
public static function checkComment($id) {
|
||||
$comment = Database::get('comments', '*', ['id' => $id]);
|
||||
if (!$id || !$comment) {
|
||||
RestApi::response(['success' => false, 'errors' => ['comment' => 'Комментарий не найден']], 404);
|
||||
}
|
||||
return $comment;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
223
backend/app/class/enity/class_fileUpload.php
Normal file
223
backend/app/class/enity/class_fileUpload.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
class FileUpload {
|
||||
|
||||
protected static $base_path = __DIR__ . '/../../../public/';
|
||||
protected static $base_url = '/public/';
|
||||
|
||||
// === Маппинг сущностей (все параметры обязательны) ===
|
||||
protected static $entities = [
|
||||
'task' => [
|
||||
'table' => 'cards_task',
|
||||
'folder' => 'task',
|
||||
'field' => 'file_img',
|
||||
'allowed_ext' => ['png', 'jpg', 'jpeg', 'zip', 'rar'],
|
||||
'archive_ext' => ['zip', 'rar'],
|
||||
'max_size' => 10 * 1024 * 1024 // 10 MB
|
||||
],
|
||||
'comment' => [
|
||||
'table' => 'comments',
|
||||
'folder' => 'comment',
|
||||
'field' => 'file_img',
|
||||
'allowed_ext' => ['png', 'jpg', 'jpeg', 'zip', 'rar'],
|
||||
'archive_ext' => ['zip', 'rar'],
|
||||
'max_size' => 10 * 1024 * 1024 // 10 MB
|
||||
]
|
||||
];
|
||||
|
||||
// Получение конфигурации сущности
|
||||
protected static function getConfig($entity_type) {
|
||||
if (!isset(self::$entities[$entity_type])) {
|
||||
return null;
|
||||
}
|
||||
return self::$entities[$entity_type];
|
||||
}
|
||||
|
||||
// Получение данных сущности из БД
|
||||
protected static function getEntityData($config, $entity_id) {
|
||||
return Database::get($config['table'], '*', ['id' => $entity_id]);
|
||||
}
|
||||
|
||||
// Обновление файлов сущности в БД
|
||||
protected static function updateEntityFiles($config, $entity_id, $files) {
|
||||
Database::update($config['table'], [
|
||||
$config['field'] => json_encode($files, JSON_UNESCAPED_UNICODE)
|
||||
], [
|
||||
'id' => $entity_id
|
||||
]);
|
||||
}
|
||||
|
||||
// Генерация уникального имени файла
|
||||
protected static function getUniqueName($upload_dir, $file_name) {
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
$base_name = pathinfo($file_name, PATHINFO_FILENAME);
|
||||
$final_name = $file_name;
|
||||
$counter = 1;
|
||||
|
||||
while (file_exists($upload_dir . '/' . $final_name)) {
|
||||
$final_name = $base_name . '_' . $counter . '.' . $ext;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $final_name;
|
||||
}
|
||||
|
||||
// Форматирование размера файла для ошибки
|
||||
protected static function formatSize($bytes) {
|
||||
if ($bytes >= 1048576) {
|
||||
return round($bytes / 1048576, 1) . ' МБ';
|
||||
}
|
||||
return round($bytes / 1024, 1) . ' КБ';
|
||||
}
|
||||
|
||||
// === ЗАГРУЗКА ФАЙЛА ===
|
||||
public static function upload($entity_type, $entity_id, $file_base64, $file_name) {
|
||||
// Получаем конфигурацию
|
||||
$config = self::getConfig($entity_type);
|
||||
if (!$config) {
|
||||
return ['success' => false, 'errors' => ['entity' => 'Неизвестный тип сущности']];
|
||||
}
|
||||
|
||||
// Проверяем что сущность существует
|
||||
$entity = self::getEntityData($config, $entity_id);
|
||||
if (!$entity) {
|
||||
return ['success' => false, 'errors' => ['entity' => 'Сущность не найдена']];
|
||||
}
|
||||
|
||||
// Декодируем base64
|
||||
$file_data = base64_decode(preg_replace('/^data:[^;]+;base64,/', '', $file_base64));
|
||||
if (!$file_data) {
|
||||
return ['success' => false, 'errors' => ['file' => 'Ошибка декодирования файла']];
|
||||
}
|
||||
|
||||
// Проверка расширения
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $config['allowed_ext'])) {
|
||||
$allowed = strtoupper(implode(', ', $config['allowed_ext']));
|
||||
return ['success' => false, 'errors' => ['file' => "Разрешены только: $allowed"]];
|
||||
}
|
||||
|
||||
// Проверка размера
|
||||
$file_size = strlen($file_data);
|
||||
if ($file_size > $config['max_size']) {
|
||||
$max_formatted = self::formatSize($config['max_size']);
|
||||
return ['success' => false, 'errors' => ['file' => "Файл слишком большой. Максимум $max_formatted"]];
|
||||
}
|
||||
|
||||
// Путь к папке
|
||||
$upload_dir = self::$base_path . $config['folder'] . '/' . $entity_id;
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0755, true);
|
||||
}
|
||||
|
||||
// Уникальное имя
|
||||
$final_name = self::getUniqueName($upload_dir, $file_name);
|
||||
|
||||
// Сохранение файла
|
||||
$file_path = $upload_dir . '/' . $final_name;
|
||||
if (!file_put_contents($file_path, $file_data)) {
|
||||
return ['success' => false, 'errors' => ['file' => 'Ошибка сохранения файла']];
|
||||
}
|
||||
|
||||
// Формируем URL
|
||||
$file_url = self::$base_url . $config['folder'] . '/' . $entity_id . '/' . $final_name;
|
||||
|
||||
// Обновляем file_img в базе
|
||||
$current_files = $entity[$config['field']] ? json_decode($entity[$config['field']], true) : [];
|
||||
$current_files[] = [
|
||||
'url' => $file_url,
|
||||
'name' => $final_name,
|
||||
'size' => $file_size
|
||||
];
|
||||
|
||||
self::updateEntityFiles($config, $entity_id, $current_files);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'file' => [
|
||||
'url' => $file_url,
|
||||
'name' => $final_name,
|
||||
'size' => $file_size
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// === УДАЛЕНИЕ ФАЙЛОВ ===
|
||||
public static function delete($entity_type, $entity_id, $file_names) {
|
||||
// Получаем конфигурацию
|
||||
$config = self::getConfig($entity_type);
|
||||
if (!$config) {
|
||||
return ['success' => false, 'errors' => ['entity' => 'Неизвестный тип сущности']];
|
||||
}
|
||||
|
||||
// Проверяем что сущность существует
|
||||
$entity = self::getEntityData($config, $entity_id);
|
||||
if (!$entity) {
|
||||
return ['success' => false, 'errors' => ['entity' => 'Сущность не найдена']];
|
||||
}
|
||||
|
||||
// Приводим к массиву если передана строка
|
||||
if (!is_array($file_names)) {
|
||||
$file_names = [$file_names];
|
||||
}
|
||||
|
||||
// Получаем текущие файлы
|
||||
$current_files = $entity[$config['field']] ? json_decode($entity[$config['field']], true) : [];
|
||||
$upload_dir = self::$base_path . $config['folder'] . '/' . $entity_id;
|
||||
$deleted = [];
|
||||
|
||||
// Удаляем каждый файл
|
||||
foreach ($file_names as $file_name) {
|
||||
foreach ($current_files as $index => $file) {
|
||||
if ($file['name'] === $file_name) {
|
||||
// Удаляем файл с диска
|
||||
$file_path = $upload_dir . '/' . $file_name;
|
||||
if (file_exists($file_path)) {
|
||||
unlink($file_path);
|
||||
}
|
||||
// Удаляем из массива
|
||||
array_splice($current_files, $index, 1);
|
||||
$deleted[] = $file_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем в базе
|
||||
self::updateEntityFiles($config, $entity_id, $current_files);
|
||||
|
||||
// Удаляем папку если она пустая
|
||||
if (is_dir($upload_dir) && count(scandir($upload_dir)) === 2) {
|
||||
rmdir($upload_dir);
|
||||
}
|
||||
|
||||
return ['success' => true, 'deleted' => $deleted];
|
||||
}
|
||||
|
||||
// === УДАЛЕНИЕ ПАПКИ СУЩНОСТИ (при удалении самой сущности) ===
|
||||
public static function deleteFolder($entity_type, $entity_id) {
|
||||
// Получаем конфигурацию
|
||||
$config = self::getConfig($entity_type);
|
||||
if (!$config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$upload_dir = self::$base_path . $config['folder'] . '/' . $entity_id;
|
||||
|
||||
if (is_dir($upload_dir)) {
|
||||
// Удаляем все файлы в папке
|
||||
$files = glob($upload_dir . '/*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
// Удаляем папку
|
||||
rmdir($upload_dir);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -6,24 +6,56 @@ class Project extends BaseEntity {
|
||||
|
||||
// Получение всех проектов
|
||||
public function getAll() {
|
||||
return Database::select($this->db_name, [
|
||||
$current_user_id = RestApi::getCurrentUserId();
|
||||
|
||||
$projects = Database::select($this->db_name, [
|
||||
'id',
|
||||
'id_order',
|
||||
'name',
|
||||
'id_ready'
|
||||
'id_ready',
|
||||
'id_admin'
|
||||
], [
|
||||
'ORDER' => ['id_order' => 'ASC']
|
||||
]);
|
||||
|
||||
// Обрабатываем id_admin для каждого проекта
|
||||
return array_map(function($project) use ($current_user_id) {
|
||||
$admins = $project['id_admin'] ? json_decode($project['id_admin'], true) : [];
|
||||
|
||||
if ($current_user_id && in_array((int)$current_user_id, $admins, true)) {
|
||||
$project['id_admin'] = true;
|
||||
} else {
|
||||
unset($project['id_admin']);
|
||||
}
|
||||
|
||||
return $project;
|
||||
}, $projects);
|
||||
}
|
||||
|
||||
// Получение одного проекта
|
||||
public static function get($id) {
|
||||
return Database::get('project', [
|
||||
// $current_user_id — ID текущего пользователя для проверки админства
|
||||
public static function get($id, $current_user_id = null) {
|
||||
$project = Database::get('project', [
|
||||
'id',
|
||||
'id_order',
|
||||
'name',
|
||||
'id_ready'
|
||||
'id_ready',
|
||||
'id_admin'
|
||||
], ['id' => $id]);
|
||||
|
||||
if ($project) {
|
||||
$admins = $project['id_admin'] ? json_decode($project['id_admin'], true) : [];
|
||||
|
||||
// Если передан user_id — проверяем админство
|
||||
if ($current_user_id && in_array((int)$current_user_id, $admins, true)) {
|
||||
$project['id_admin'] = true;
|
||||
} else {
|
||||
// Не админ — убираем поле
|
||||
unset($project['id_admin']);
|
||||
}
|
||||
}
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
// Получение id_ready (ID колонки "Готово") по ID проекта
|
||||
@@ -58,7 +90,10 @@ class Project extends BaseEntity {
|
||||
|
||||
// Получение всех данных проекта (проект + колонки + отделы + метки)
|
||||
public static function getProjectData($project_id) {
|
||||
$project = self::get($project_id);
|
||||
// Получаем ID текущего пользователя для проверки админства
|
||||
$current_user_id = RestApi::getCurrentUserId();
|
||||
|
||||
$project = self::get($project_id, $current_user_id);
|
||||
if (!$project) {
|
||||
return null;
|
||||
}
|
||||
@@ -78,6 +113,17 @@ class Project extends BaseEntity {
|
||||
'labels' => $labels
|
||||
];
|
||||
}
|
||||
|
||||
// Проверка является ли пользователь админом проекта
|
||||
public static function isAdmin($project_id, $user_id): bool {
|
||||
$project = Database::get('project', ['id_admin'], ['id' => $project_id]);
|
||||
if (!$project || !$project['id_admin']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$admins = json_decode($project['id_admin'], true) ?: [];
|
||||
return in_array((int)$user_id, $admins, true);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -84,7 +84,7 @@ class Task extends BaseEntity {
|
||||
$uploaded_files = [];
|
||||
if (!empty($files)) {
|
||||
foreach ($files as $file) {
|
||||
$result = TaskImage::upload($this->id, $file['data'], $file['name']);
|
||||
$result = FileUpload::upload('task', $this->id, $file['data'], $file['name']);
|
||||
if ($result['success']) {
|
||||
$uploaded_files[] = $result['file'];
|
||||
}
|
||||
@@ -154,24 +154,40 @@ class Task extends BaseEntity {
|
||||
// Проверка что задача существует
|
||||
self::check_task($id);
|
||||
|
||||
// Удаляем папку с файлами если есть
|
||||
$upload_dir = __DIR__ . '/../../../public/task/' . $id;
|
||||
if (is_dir($upload_dir)) {
|
||||
$files = glob($upload_dir . '/*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
// Удаляем папку с файлами задачи
|
||||
FileUpload::deleteFolder('task', $id);
|
||||
|
||||
// Удаляем все комментарии задачи и их файлы
|
||||
$comments = Database::select('comments', ['id'], ['id_task' => $id]);
|
||||
if ($comments) {
|
||||
foreach ($comments as $comment) {
|
||||
FileUpload::deleteFolder('comment', $comment['id']);
|
||||
}
|
||||
rmdir($upload_dir);
|
||||
Database::delete('comments', ['id_task' => $id]);
|
||||
}
|
||||
|
||||
// Удаляем из базы
|
||||
// Удаляем задачу из базы
|
||||
Database::delete('cards_task', ['id' => $id]);
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
// === МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ ===
|
||||
|
||||
// Загрузка файла к задаче
|
||||
public static function uploadFile($task_id, $file_base64, $file_name) {
|
||||
// Проверка что задача существует
|
||||
self::check_task($task_id);
|
||||
return FileUpload::upload('task', $task_id, $file_base64, $file_name);
|
||||
}
|
||||
|
||||
// Удаление файлов задачи
|
||||
public static function deleteFile($task_id, $file_names) {
|
||||
// Проверка что задача существует
|
||||
self::check_task($task_id);
|
||||
return FileUpload::delete('task', $task_id, $file_names);
|
||||
}
|
||||
|
||||
// Изменение порядка и колонки задачи (с пересчётом order)
|
||||
public static function updateOrder($id, $column_id, $to_index) {
|
||||
|
||||
@@ -251,8 +267,20 @@ class Task extends BaseEntity {
|
||||
'archive'
|
||||
], $where);
|
||||
|
||||
// Получаем количество комментариев для всех задач одним запросом
|
||||
$task_ids = array_column($tasks, 'id');
|
||||
$comments_counts = [];
|
||||
if (!empty($task_ids)) {
|
||||
$counts = Database::query(
|
||||
"SELECT id_task, COUNT(*) as cnt FROM comments WHERE id_task IN (" . implode(',', $task_ids) . ") GROUP BY id_task"
|
||||
)->fetchAll(\PDO::FETCH_ASSOC);
|
||||
foreach ($counts as $row) {
|
||||
$comments_counts[$row['id_task']] = (int)$row['cnt'];
|
||||
}
|
||||
}
|
||||
|
||||
// Декодируем JSON и получаем avatar_url из accounts
|
||||
return array_map(function($task) {
|
||||
return array_map(function($task) use ($comments_counts) {
|
||||
$task['file_img'] = $task['file_img'] ? json_decode($task['file_img'], true) : [];
|
||||
|
||||
// Получаем avatar_url из accounts по id_account
|
||||
@@ -263,6 +291,9 @@ class Task extends BaseEntity {
|
||||
$task['avatar_img'] = null;
|
||||
}
|
||||
|
||||
// Количество комментариев
|
||||
$task['comments_count'] = $comments_counts[$task['id']] ?? 0;
|
||||
|
||||
return $task;
|
||||
}, $tasks);
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
<?php
|
||||
|
||||
class TaskImage {
|
||||
|
||||
protected static $db_name = 'cards_task';
|
||||
protected static $upload_path = '/public/task/';
|
||||
|
||||
// Валидация всех данных для загрузки
|
||||
protected static function validate($task_id, $file_base64, $file_name) {
|
||||
// Проверка и получение задачи
|
||||
$task = Task::check_task($task_id);
|
||||
|
||||
// Декодируем base64 (убираем любой data: префикс)
|
||||
$file_data = base64_decode(preg_replace('/^data:[^;]+;base64,/', '', $file_base64));
|
||||
if (!$file_data) {
|
||||
return ['success' => false, 'errors' => ['file' => 'Ошибка декодирования файла']];
|
||||
}
|
||||
|
||||
// Проверка расширения
|
||||
$allowed_ext = ['png', 'jpg', 'jpeg', 'zip', 'rar'];
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowed_ext)) {
|
||||
return ['success' => false, 'errors' => ['file' => 'Разрешены только PNG, JPG, JPEG, ZIP, RAR']];
|
||||
}
|
||||
|
||||
// Проверка размера (10 МБ)
|
||||
$max_size = 10 * 1024 * 1024;
|
||||
if (strlen($file_data) > $max_size) {
|
||||
return ['success' => false, 'errors' => ['file' => 'Файл слишком большой. Максимум 10 МБ']];
|
||||
}
|
||||
|
||||
// Всё ок — возвращаем данные
|
||||
return [
|
||||
'task' => $task,
|
||||
'file_data' => $file_data,
|
||||
'is_archive' => in_array($ext, ['zip', 'rar'])
|
||||
];
|
||||
}
|
||||
|
||||
// Генерация уникального имени файла
|
||||
protected static function getUniqueName($upload_dir, $file_name) {
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
$base_name = pathinfo($file_name, PATHINFO_FILENAME);
|
||||
$final_name = $file_name;
|
||||
$counter = 1;
|
||||
|
||||
while (file_exists($upload_dir . '/' . $final_name)) {
|
||||
$final_name = $base_name . '_' . $counter . '.' . $ext;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $final_name;
|
||||
}
|
||||
|
||||
// Загрузка изображения
|
||||
public static function upload($task_id, $file_base64, $file_name) {
|
||||
// Валидация
|
||||
$validation = self::validate($task_id, $file_base64, $file_name);
|
||||
if (isset($validation['success'])) return $validation;
|
||||
|
||||
$task = $validation['task'];
|
||||
$file_data = $validation['file_data'];
|
||||
|
||||
// Путь к папке
|
||||
$upload_dir = __DIR__ . '/../../../public/task/' . $task_id;
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0755, true);
|
||||
}
|
||||
|
||||
// Уникальное имя
|
||||
$final_name = self::getUniqueName($upload_dir, $file_name);
|
||||
|
||||
// Сохранение файла
|
||||
$file_path = $upload_dir . '/' . $final_name;
|
||||
if (!file_put_contents($file_path, $file_data)) {
|
||||
return ['success' => false, 'errors' => ['file' => 'Ошибка сохранения файла']];
|
||||
}
|
||||
|
||||
// Формируем URL
|
||||
$file_url = self::$upload_path . $task_id . '/' . $final_name;
|
||||
$file_size = strlen($file_data);
|
||||
|
||||
// Обновляем file_img в базе
|
||||
$current_files = $task['file_img'] ? json_decode($task['file_img'], true) : [];
|
||||
$current_files[] = [
|
||||
'url' => $file_url,
|
||||
'name' => $final_name,
|
||||
'size' => $file_size
|
||||
];
|
||||
|
||||
Database::update(self::$db_name, [
|
||||
'file_img' => json_encode($current_files, JSON_UNESCAPED_UNICODE)
|
||||
], [
|
||||
'id' => $task_id
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'file' => [
|
||||
'url' => $file_url,
|
||||
'name' => $final_name,
|
||||
'size' => $file_size
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Удаление изображений (принимает строку или массив имён файлов)
|
||||
public static function delete($task_id, $file_names) {
|
||||
// Проверка и получение задачи
|
||||
$task = Task::check_task($task_id);
|
||||
|
||||
// Приводим к массиву если передана строка
|
||||
if (!is_array($file_names)) {
|
||||
$file_names = [$file_names];
|
||||
}
|
||||
|
||||
// Получаем текущие файлы
|
||||
$current_files = $task['file_img'] ? json_decode($task['file_img'], true) : [];
|
||||
$upload_dir = __DIR__ . '/../../../public/task/' . $task_id;
|
||||
$deleted = [];
|
||||
|
||||
// Удаляем каждый файл
|
||||
foreach ($file_names as $file_name) {
|
||||
// Ищем файл в массиве
|
||||
foreach ($current_files as $index => $file) {
|
||||
if ($file['name'] === $file_name) {
|
||||
// Удаляем файл с диска
|
||||
$file_path = $upload_dir . '/' . $file_name;
|
||||
if (file_exists($file_path)) {
|
||||
unlink($file_path);
|
||||
}
|
||||
// Удаляем из массива
|
||||
array_splice($current_files, $index, 1);
|
||||
$deleted[] = $file_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем в базе
|
||||
Database::update(self::$db_name, [
|
||||
'file_img' => json_encode($current_files, JSON_UNESCAPED_UNICODE)
|
||||
], [
|
||||
'id' => $task_id
|
||||
]);
|
||||
|
||||
// Удаляем папку если она пустая
|
||||
if (is_dir($upload_dir) && count(scandir($upload_dir)) === 2) {
|
||||
rmdir($upload_dir);
|
||||
}
|
||||
|
||||
return ['success' => true, 'deleted' => $deleted];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
// Часовой пояс сервера
|
||||
date_default_timezone_set('Europe/Moscow');
|
||||
|
||||
// Подключение классов базы данных
|
||||
require_once __DIR__ . '/class/database/class_Medoo.php';
|
||||
require_once __DIR__ . '/class/database/class_Database.php';
|
||||
@@ -11,10 +14,11 @@
|
||||
// подключение классов REST API и Entity
|
||||
require_once __DIR__ . '/restAPI/class_restApi.php';
|
||||
require_once __DIR__ . '/class/enity/class_base.php';
|
||||
require_once __DIR__ . '/class/enity/class_fileUpload.php';
|
||||
require_once __DIR__ . '/class/enity/class_user.php';
|
||||
require_once __DIR__ . '/class/enity/class_project.php';
|
||||
require_once __DIR__ . '/class/enity/class_task.php';
|
||||
require_once __DIR__ . '/class/enity/class_taskImage.php';
|
||||
require_once __DIR__ . '/class/enity/class_comment.php';
|
||||
|
||||
// Данные подключения к БД
|
||||
define('DB_HOST', '192.168.1.9');
|
||||
@@ -31,8 +35,10 @@
|
||||
'/api/user' => __DIR__ . '/../api/user.php',
|
||||
'/api/task' => __DIR__ . '/../api/task.php',
|
||||
'/api/project' => __DIR__ . '/../api/project.php',
|
||||
'/api/comment' => __DIR__ . '/../api/comment.php',
|
||||
'/api/server' => __DIR__ . '/../api/server.php',
|
||||
];
|
||||
|
||||
$publicActions = ['auth_login', 'check_session'];
|
||||
$publicActions = ['auth_login', 'check_session', 'get_settings'];
|
||||
|
||||
?>
|
||||
@@ -18,6 +18,21 @@ class RestApi {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Получить ID текущего авторизованного пользователя
|
||||
public static function getCurrentUserId(): ?int {
|
||||
$session = $_COOKIE['session'] ?? null;
|
||||
if (!$session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sessionData = Database::get('accounts_session', ['id_accounts'], [
|
||||
'keycookies' => $session,
|
||||
'data_closed[>]' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
return $sessionData ? (int)$sessionData['id_accounts'] : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user