kokjan/backend/modules/administrator/controllers/FilemanagerController.php

270 lines
11 KiB
PHP
Raw Normal View History

<?php
namespace backend\modules\administrator\controllers;
use Yii;
use yii\web\Controller;
use yii\web\UploadedFile;
use yii\helpers\FileHelper;
use yii\filters\AccessControl;
use yii\filters\VerbFilter;
use yii\web\NotFoundHttpException;
use yii\imagine\Image;
use Imagine\Image\Box;
/**
* FilemanagerController handles secure file management.
*/
class FilemanagerController extends Controller
{
// Configuration
protected $maxFileSize = 20971520; // 20MB
protected $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'zip', 'rar', '7z', 'txt', 'mp4', 'mp3'];
protected $allowedMimeTypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
'text/plain', 'video/mp4', 'audio/mpeg'
];
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'create-folder' => ['post'],
'upload' => ['post'],
'delete' => ['post'],
],
],
];
}
protected function getBaseUploadPath()
{
$path = Yii::getAlias('@backend/web/_uploads');
if (!is_dir($path)) {
FileHelper::createDirectory($path, 0777);
}
return $path;
}
protected function getRequestedPath($path = '')
{
$base = realpath($this->getBaseUploadPath());
if (empty($path)) return $base;
$path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
$requested = realpath($base . DIRECTORY_SEPARATOR . $path);
if ($requested === false) {
$requested = $base . DIRECTORY_SEPARATOR . $path;
}
if (strpos($requested, $base) !== 0) {
throw new NotFoundHttpException('Access denied.');
}
return $requested;
}
public function actionIndex($path = '')
{
try {
$currentFullPath = $this->getRequestedPath($path);
if (!is_dir($currentFullPath)) {
FileHelper::createDirectory($currentFullPath, 0777);
}
$dirs = [];
$items = scandir($currentFullPath);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$fullItem = $currentFullPath . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullItem)) {
$dirs[] = [
'name' => $item,
'relPath' => trim($path . '/' . $item, '/'),
'time' => date('Y-m-d H:i:s', filemtime($fullItem)),
];
}
}
$fileList = [];
$files = FileHelper::findFiles($currentFullPath, ['recursive' => false]);
foreach ($files as $file) {
$fileList[] = [
'name' => basename($file),
'size' => Yii::$app->formatter->asShortSize(filesize($file)),
'time' => date('Y-m-d H:i:s', filemtime($file)),
'ext' => pathinfo($file, PATHINFO_EXTENSION),
'relPath' => trim($path . '/' . basename($file), '/'),
];
}
usort($fileList, function($a, $b) {
return strtotime($b['time']) - strtotime($a['time']);
});
return $this->render('index', [
'dirs' => $dirs,
'files' => $fileList,
'currentPath' => $path,
]);
} catch (\Exception $e) {
Yii::$app->session->setFlash('error', "System Error: " . $e->getMessage());
return $this->redirect(['index']);
}
}
public function actionCreateFolder($path = '')
{
try {
$parentPath = $this->getRequestedPath($path);
$folderName = Yii::$app->request->post('folder_name');
if (!$folderName) {
Yii::$app->session->setFlash('error', "กรุณาระบุชื่อโฟลเดอร์");
return $this->redirect(['index', 'path' => $path]);
}
// Secure folder name
$folderName = preg_replace('/[^a-zA-Z0-9_\-\x{0E00}-\x{0E7F}]/u', '_', $folderName);
$newPath = $parentPath . DIRECTORY_SEPARATOR . $folderName;
if (!file_exists($newPath)) {
if (FileHelper::createDirectory($newPath, 0777)) {
Yii::$app->session->setFlash('success', "สร้างโฟลเดอร์ '$folderName' สำเร็จ");
}
} else {
Yii::$app->session->setFlash('warning', "มีโฟลเดอร์ชื่อนี้อยู่แล้ว");
}
} catch (\Exception $e) {
Yii::$app->session->setFlash('error', "Error: " . $e->getMessage());
}
return $this->redirect(['index', 'path' => $path]);
}
public function actionUpload($path = '')
{
try {
$targetFullPath = $this->getRequestedPath($path);
if (Yii::$app->request->isPost) {
$files = UploadedFile::getInstancesByName('files');
$requestedWidth = Yii::$app->request->post('resize_width', '1024');
$success = 0;
$errors = [];
foreach ($files as $file) {
// 1. Check Extension
if (!in_array(strtolower($file->extension), $this->allowedExtensions)) {
$errors[] = "{$file->name}: ไม่อนุญาตให้อัปโหลดไฟล์นามสกุลนี้";
continue;
}
// 2. Check MIME Type (Content sniffing)
$mimeType = FileHelper::getMimeType($file->tempName);
if (!in_array($mimeType, $this->allowedMimeTypes)) {
$errors[] = "{$file->name}: เนื้อหาไฟล์ไม่ถูกต้องตามมาตรฐานความปลอดภัย";
continue;
}
// 3. Check File Size
if ($file->size > $this->maxFileSize) {
$errors[] = "{$file->name}: ขนาดไฟล์เกินขีดจำกัด (20MB)";
continue;
}
// 4. Sanitize Filename
$safeName = preg_replace('/[^a-zA-Z0-9_\-\.]/u', '_', $file->baseName);
$fileName = $safeName . '.' . strtolower($file->extension);
if (file_exists($targetFullPath . DIRECTORY_SEPARATOR . $fileName)) {
$fileName = $safeName . '_' . time() . '.' . strtolower($file->extension);
}
$targetFile = $targetFullPath . DIRECTORY_SEPARATOR . $fileName;
$isImage = in_array($mimeType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
if ($isImage && $requestedWidth !== 'original') {
$imagine = Image::getImagine();
$image = $imagine->open($file->tempName);
$size = $image->getSize();
$maxWidth = (int)$requestedWidth;
if ($size->getWidth() > $maxWidth) {
Image::thumbnail($file->tempName, $maxWidth, null)
->save($targetFile, ['quality' => 85]);
} else {
$file->saveAs($targetFile);
}
$success++;
} else {
if ($file->saveAs($targetFile)) {
$success++;
} else {
$errors[] = "{$file->name}: บันทึกไฟล์ล้มเหลว";
}
}
}
if ($success > 0) Yii::$app->session->setFlash('success', "อัปโหลดและตรวจสอบความปลอดภัยสำเร็จ $success ไฟล์");
if (!empty($errors)) Yii::$app->session->setFlash('error', "พบข้อผิดพลาด: " . implode('<br>', $errors));
}
} catch (\Exception $e) {
Yii::$app->session->setFlash('error', "Security Error: " . $e->getMessage());
}
return $this->redirect(['index', 'path' => $path]);
}
public function actionDelete($relPath)
{
try {
$fullPath = $this->getRequestedPath($relPath);
$parentRelPath = dirname($relPath);
if ($parentRelPath === '.') $parentRelPath = '';
if (is_dir($fullPath)) {
$files = array_diff(scandir($fullPath), array('.', '..'));
if (empty($files)) {
if (rmdir($fullPath)) Yii::$app->session->setFlash('success', "ลบโฟลเดอร์เรียบร้อยแล้ว");
} else {
Yii::$app->session->setFlash('error', "ไม่สามารถลบได้ เนื่องจากในโฟลเดอร์ยังมีไฟล์อยู่");
}
} else {
if (unlink($fullPath)) Yii::$app->session->setFlash('success', "ลบไฟล์เรียบร้อยแล้ว");
}
return $this->redirect(['index', 'path' => $parentRelPath]);
} catch (\Exception $e) {
Yii::$app->session->setFlash('error', "Delete Error: " . $e->getMessage());
return $this->redirect(['index']);
}
}
public function actionDownload($relPath)
{
try {
$fullPath = $this->getRequestedPath($relPath);
if (file_exists($fullPath) && !is_dir($fullPath)) {
return Yii::$app->response->sendFile($fullPath);
}
} catch (\Exception $e) {
throw new NotFoundHttpException('Not found.');
}
throw new NotFoundHttpException('Not found.');
}
}