src/Eccube/Controller/Admin/Content/FileController.php line 57

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of EC-CUBE
  4.  *
  5.  * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
  6.  *
  7.  * http://www.ec-cube.co.jp/
  8.  *
  9.  * For the full copyright and license information, please view the LICENSE
  10.  * file that was distributed with this source code.
  11.  */
  12. namespace Eccube\Controller\Admin\Content;
  13. use Eccube\Controller\AbstractController;
  14. use Eccube\Util\FilesystemUtil;
  15. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  16. use Symfony\Component\Filesystem\Exception\IOException;
  17. use Symfony\Component\Filesystem\Filesystem;
  18. use Symfony\Component\Finder\Finder;
  19. use Symfony\Component\Form\Extension\Core\Type\FileType;
  20. use Symfony\Component\Form\Extension\Core\Type\FormType;
  21. use Symfony\Component\Form\Extension\Core\Type\TextType;
  22. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  23. use Symfony\Component\HttpFoundation\File\Exception\FileException;
  24. use Symfony\Component\HttpFoundation\File\UploadedFile;
  25. use Symfony\Component\HttpFoundation\Request;
  26. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  27. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  28. use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
  29. use Symfony\Component\Routing\Annotation\Route;
  30. use Symfony\Component\Validator\Constraints as Assert;
  31. class FileController extends AbstractController
  32. {
  33.     public const SJIS 'sjis-win';
  34.     public const UTF 'UTF-8';
  35.     private $errors = [];
  36.     private $encode;
  37.     /**
  38.      * FileController constructor.
  39.      */
  40.     public function __construct()
  41.     {
  42.         $this->encode self::UTF;
  43.         if ('\\' === DIRECTORY_SEPARATOR) {
  44.             $this->encode self::SJIS;
  45.         }
  46.     }
  47.     /**
  48.      * @Route("/%eccube_admin_route%/content/file_manager", name="admin_content_file", methods={"GET", "POST"})
  49.      * @Template("@admin/Content/file.twig")
  50.      */
  51.     public function index(Request $request)
  52.     {
  53.         $this->addInfoOnce('admin.common.restrict_file_upload_info''admin');
  54.         $form $this->formFactory->createBuilder(FormType::class)
  55.             ->add('file'FileType::class, [
  56.                 'multiple' => true,
  57.                 'attr' => [
  58.                     'multiple' => 'multiple',
  59.                 ],
  60.             ])
  61.             ->add('create_file'TextType::class)
  62.             ->getForm();
  63.         // user_data_dir
  64.         $userDataDir $this->getUserDataDir();
  65.         $topDir $this->normalizePath($userDataDir);
  66.         //        $topDir = '/';
  67.         // user_data_dirの親ディレクトリ
  68.         $htmlDir $this->normalizePath($this->getUserDataDir().'/../');
  69.         // カレントディレクトリ
  70.         $nowDir $this->checkDir($this->getUserDataDir($request->get('tree_select_file')), $this->getUserDataDir())
  71.             ? $this->normalizePath($this->getUserDataDir($request->get('tree_select_file')))
  72.             : $topDir;
  73.         // パンくず表示用データ
  74.         $nowDirList json_encode(explode('/'trim(str_replace($htmlDir''$nowDir), '/')));
  75.         $jailNowDir $this->getJailDir($nowDir);
  76.         $isTopDir = ($topDir === $jailNowDir);
  77.         $parentDir substr($nowDir0strrpos($nowDir'/'));
  78.         if ('POST' === $request->getMethod()) {
  79.             switch ($request->get('mode')) {
  80.                 case 'create':
  81.                     $this->create($request);
  82.                     break;
  83.                 case 'upload':
  84.                     $this->upload($request);
  85.                     break;
  86.                 default:
  87.                     break;
  88.             }
  89.         }
  90.         $tree $this->getTree($this->getUserDataDir(), $request);
  91.         $arrFileList $this->getFileList($nowDir);
  92.         $paths $this->getPathsToArray($tree);
  93.         $tree $this->getTreeToArray($tree);
  94.         return [
  95.             'form' => $form->createView(),
  96.             'tpl_javascript' => json_encode($tree),
  97.             'top_dir' => $this->getJailDir($topDir),
  98.             'tpl_is_top_dir' => $isTopDir,
  99.             'tpl_now_dir' => $jailNowDir,
  100.             'html_dir' => $this->getJailDir($htmlDir),
  101.             'now_dir_list' => $nowDirList,
  102.             'tpl_parent_dir' => $this->getJailDir($parentDir),
  103.             'arrFileList' => $arrFileList,
  104.             'errors' => $this->errors,
  105.             'paths' => json_encode($paths),
  106.         ];
  107.     }
  108.     /**
  109.      * @Route("/%eccube_admin_route%/content/file_view", name="admin_content_file_view", methods={"GET"})
  110.      */
  111.     public function view(Request $request)
  112.     {
  113.         $file $this->convertStrToServer($this->getUserDataDir($request->get('file')));
  114.         if ($this->checkDir($file$this->getUserDataDir())) {
  115.             setlocale(LC_ALL'ja_JP.UTF-8');
  116.             return new BinaryFileResponse($file);
  117.         }
  118.         throw new NotFoundHttpException();
  119.     }
  120.     /**
  121.      * Create directory
  122.      *
  123.      * @param Request $request
  124.      */
  125.     public function create(Request $request)
  126.     {
  127.         $form $this->formFactory->createBuilder(FormType::class)
  128.             ->add('file'FileType::class, [
  129.                 'multiple' => true,
  130.                 'attr' => [
  131.                     'multiple' => 'multiple',
  132.                 ],
  133.             ])
  134.             ->add('create_file'TextType::class, [
  135.                 'constraints' => [
  136.                     new Assert\NotBlank(),
  137.                     new Assert\Regex([
  138.                         'pattern' => '/[^[:alnum:]_.\\-]/',
  139.                         'match' => false,
  140.                         'message' => 'admin.content.file.folder_name_symbol_error',
  141.                     ]),
  142.                     new Assert\Regex([
  143.                         'pattern' => "/^\.(.*)$/",
  144.                         'match' => false,
  145.                         'message' => 'admin.content.file.folder_name_period_error',
  146.                     ]),
  147.                 ],
  148.             ])
  149.             ->getForm();
  150.         $form->handleRequest($request);
  151.         if (!$form->isValid()) {
  152.             foreach ($form->getErrors(true) as $error) {
  153.                 $this->errors[] = ['message' => $error->getMessage()];
  154.             }
  155.             return;
  156.         }
  157.         $fs = new Filesystem();
  158.         $filename $form->get('create_file')->getData();
  159.         try {
  160.             $topDir $this->getUserDataDir();
  161.             $nowDir $this->getUserDataDir($request->get('now_dir'));
  162.             $nowDir $this->checkDir($nowDir$topDir)
  163.                 ? $this->normalizePath($nowDir)
  164.                 : $topDir;
  165.             $newFilePath $nowDir.'/'.$filename;
  166.             if (file_exists($newFilePath)) {
  167.                 throw new IOException(trans('admin.content.file.dir_exists', ['%file_name%' => $filename]));
  168.             }
  169.         } catch (IOException $e) {
  170.             $this->errors[] = ['message' => $e->getMessage()];
  171.             return;
  172.         }
  173.         try {
  174.             $fs->mkdir($newFilePath);
  175.             $this->addSuccess('admin.common.create_complete''admin');
  176.         } catch (IOException $e) {
  177.             log_error($e->getMessage());
  178.             $this->errors[] = ['message' => trans('admin.content.file.upload_error', [
  179.                 '%file_name%' => $filename,
  180.             ])];
  181.         }
  182.     }
  183.     /**
  184.      * @Route("/%eccube_admin_route%/content/file_delete", name="admin_content_file_delete", methods={"DELETE"})
  185.      */
  186.     public function delete(Request $request)
  187.     {
  188.         $this->isTokenValid();
  189.         $selectFile $request->get('select_file');
  190.         if ($selectFile === '' || $selectFile === null || $selectFile == '/') {
  191.             return $this->redirectToRoute('admin_content_file');
  192.         }
  193.         $topDir $this->getUserDataDir();
  194.         $file $this->convertStrToServer($this->getUserDataDir($selectFile));
  195.         if ($this->checkDir($file$topDir)) {
  196.             $fs = new Filesystem();
  197.             if ($fs->exists($file)) {
  198.                 $fs->remove($file);
  199.                 $this->addSuccess('admin.common.delete_complete''admin');
  200.             }
  201.         }
  202.         // 削除実行時のカレントディレクトリを表示させる
  203.         return $this->redirectToRoute('admin_content_file', ['tree_select_file' => dirname($selectFile)]);
  204.     }
  205.     /**
  206.      * @Route("/%eccube_admin_route%/content/file_download", name="admin_content_file_download", methods={"GET"})
  207.      */
  208.     public function download(Request $request)
  209.     {
  210.         $topDir $this->getUserDataDir();
  211.         $file $this->convertStrToServer($this->getUserDataDir($request->get('select_file')));
  212.         if ($this->checkDir($file$topDir)) {
  213.             if (!is_dir($file)) {
  214.                 setlocale(LC_ALL'ja_JP.UTF-8');
  215.                 $pathParts pathinfo($file);
  216.                 $patterns = [
  217.                     '/[a-zA-Z0-9!"#$%&()=~^|@`:*;+{}]/',
  218.                     '/[- ,.<>?_[\]\/\\\\]/',
  219.                     "/['\r\n\t\v\f]/",
  220.                 ];
  221.                 $str preg_replace($patterns''$pathParts['basename']);
  222.                 if (strlen($str) === 0) {
  223.                     return (new BinaryFileResponse($file))->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT);
  224.                 } else {
  225.                     return new BinaryFileResponse($file200, [
  226.                         'Content-Type' => 'aplication/octet-stream;',
  227.                         'Content-Disposition' => "attachment; filename*=UTF-8\'\'".rawurlencode($this->convertStrFromServer($pathParts['basename'])),
  228.                     ]);
  229.                 }
  230.             }
  231.         }
  232.         throw new NotFoundHttpException();
  233.     }
  234.     public function upload(Request $request)
  235.     {
  236.         $form $this->formFactory->createBuilder(FormType::class)
  237.             ->add('file'FileType::class, [
  238.                 'multiple' => true,
  239.                 'constraints' => [
  240.                     new Assert\NotBlank([
  241.                         'message' => 'admin.common.file_select_empty',
  242.                     ]),
  243.                 ],
  244.             ])
  245.             ->add('create_file'TextType::class)
  246.             ->getForm();
  247.         $form->handleRequest($request);
  248.         if (!$form->isValid()) {
  249.             foreach ($form->getErrors(true) as $error) {
  250.                 $this->errors[] = ['message' => $error->getMessage()];
  251.             }
  252.             return;
  253.         }
  254.         $data $form->getData();
  255.         $topDir $this->getUserDataDir();
  256.         $nowDir $this->getUserDataDir($request->get('now_dir'));
  257.         if (!$this->checkDir($nowDir$topDir)) {
  258.             $this->errors[] = ['message' => 'file.text.error.invalid_upload_folder'];
  259.             return;
  260.         }
  261.         $uploadCount count($data['file']);
  262.         $successCount 0;
  263.         /** @var UploadedFile $file */
  264.         foreach ($data['file'] as $file) {
  265.             $filename $this->convertStrToServer($file->getClientOriginalName());
  266.             try {
  267.                 // フォルダの存在チェック
  268.                 if (is_dir(rtrim($nowDir'/\\').\DIRECTORY_SEPARATOR.$filename)) {
  269.                     throw new UnsupportedMediaTypeHttpException(trans('admin.content.file.same_name_folder_exists'));
  270.                 }
  271.                 // 英数字, 半角スペース, _-.() のみ許可
  272.                 if (!preg_match('/\A[a-zA-Z0-9_\-\.\(\) ]+\Z/'$filename)) {
  273.                     throw new UnsupportedMediaTypeHttpException(trans('admin.content.file.folder_name_symbol_error'));
  274.                 }
  275.                 // dotファイルはアップロード不可
  276.                 if (strpos($filename'.') === 0) {
  277.                     throw new UnsupportedMediaTypeHttpException(trans('admin.content.file.dotfile_error'));
  278.                 }
  279.                 // 許可した拡張子以外アップロード不可
  280.                 if (!in_array(strtolower($file->getClientOriginalExtension()), $this->eccubeConfig['eccube_file_uploadable_extensions'], true)) {
  281.                     throw new UnsupportedMediaTypeHttpException(trans('admin.content.file.extension_error'));
  282.                 }
  283.             } catch (UnsupportedMediaTypeHttpException $e) {
  284.                 if (!in_array($e->getMessage(), array_column($this->errors'message'))) {
  285.                     $this->errors[] = ['message' => $e->getMessage()];
  286.                 }
  287.                 continue;
  288.             }
  289.             try {
  290.                 $file->move($nowDir$filename);
  291.                 $successCount++;
  292.             } catch (FileException $e) {
  293.                 log_error($e->getMessage());
  294.                 $this->errors[] = ['message' => trans('admin.content.file.upload_error', [
  295.                     '%file_name%' => $filename,
  296.                 ])];
  297.             }
  298.         }
  299.         if ($successCount 0) {
  300.             $this->addSuccess(trans('admin.content.file.upload_complete', [
  301.                 '%success%' => $successCount,
  302.                 '%count%' => $uploadCount,
  303.             ]), 'admin');
  304.         }
  305.     }
  306.     private function getTreeToArray($tree)
  307.     {
  308.         $arrTree = [];
  309.         foreach ($tree as $key => $val) {
  310.             $path $this->getJailDir($val['path']);
  311.             $arrTree[$key] = [
  312.                 $key,
  313.                 $val['type'],
  314.                 $path,
  315.                 $val['depth'],
  316.                 $val['open'] ? 'true' 'false',
  317.             ];
  318.         }
  319.         return $arrTree;
  320.     }
  321.     private function getPathsToArray($tree)
  322.     {
  323.         $paths = [];
  324.         foreach ($tree as $val) {
  325.             $paths[] = $this->getJailDir($val['path']);
  326.         }
  327.         return $paths;
  328.     }
  329.     /**
  330.      * @param string $topDir
  331.      * @param Request $request
  332.      */
  333.     private function getTree($topDir$request)
  334.     {
  335.         $finder Finder::create()->in($topDir)
  336.             ->directories()
  337.             ->sortByName();
  338.         $tree = [];
  339.         $tree[] = [
  340.             'path' => $topDir,
  341.             'type' => '_parent',
  342.             'depth' => 0,
  343.             'open' => true,
  344.         ];
  345.         $defaultDepth count(explode('/'$topDir));
  346.         $openDirs = [];
  347.         if ($request->get('tree_status')) {
  348.             $openDirs explode('|'$request->get('tree_status'));
  349.         }
  350.         foreach ($finder as $dirs) {
  351.             $path $this->normalizePath($dirs->getRealPath());
  352.             $type = (iterator_count(Finder::create()->in($path)->directories())) ? '_parent' '_child';
  353.             $depth count(explode('/'$path)) - $defaultDepth;
  354.             $tree[] = [
  355.                 'path' => $path,
  356.                 'type' => $type,
  357.                 'depth' => $depth,
  358.                 'open' => (in_array($path$openDirs)) ? true false,
  359.             ];
  360.         }
  361.         return $tree;
  362.     }
  363.     /**
  364.      * @param string $nowDir
  365.      */
  366.     private function getFileList($nowDir)
  367.     {
  368.         $topDir $this->getuserDataDir();
  369.         $filter = function (\SplFileInfo $file) use ($topDir) {
  370.             $acceptPath realpath($topDir);
  371.             $targetPath $file->getRealPath();
  372.             return strpos($targetPath$acceptPath) === 0;
  373.         };
  374.         $finder Finder::create()
  375.             ->filter($filter)
  376.             ->in($nowDir)
  377.             ->ignoreDotFiles(false)
  378.             ->sortByName()
  379.             ->depth(0);
  380.         $dirFinder $finder->directories();
  381.         try {
  382.             $dirs $dirFinder->getIterator();
  383.         } catch (\Exception $e) {
  384.             $dirs = [];
  385.         }
  386.         $fileFinder $finder->files();
  387.         try {
  388.             $files $fileFinder->getIterator();
  389.         } catch (\Exception $e) {
  390.             $files = [];
  391.         }
  392.         $arrFileList = [];
  393.         foreach ($dirs as $dir) {
  394.             $dirPath $this->normalizePath($dir->getRealPath());
  395.             $childDir Finder::create()
  396.                 ->in($dirPath)
  397.                 ->ignoreDotFiles(false)
  398.                 ->directories()
  399.                 ->depth(0);
  400.             $childFile Finder::create()
  401.                 ->in($dirPath)
  402.                 ->ignoreDotFiles(false)
  403.                 ->files()
  404.                 ->depth(0);
  405.             $countNumber $childDir->count() + $childFile->count();
  406.             $arrFileList[] = [
  407.                 'file_name' => $this->convertStrFromServer($dir->getFilename()),
  408.                 'file_path' => $this->convertStrFromServer($this->getJailDir($dirPath)),
  409.                 'file_size' => FilesystemUtil::sizeToHumanReadable($dir->getSize()),
  410.                 'file_time' => $dir->getmTime(),
  411.                 'is_dir' => true,
  412.                 'is_empty' => $countNumber == true false,
  413.             ];
  414.         }
  415.         foreach ($files as $file) {
  416.             $arrFileList[] = [
  417.                 'file_name' => $this->convertStrFromServer($file->getFilename()),
  418.                 'file_path' => $this->convertStrFromServer($this->getJailDir($this->normalizePath($file->getRealPath()))),
  419.                 'file_size' => FilesystemUtil::sizeToHumanReadable($file->getSize()),
  420.                 'file_time' => $file->getmTime(),
  421.                 'is_dir' => false,
  422.                 'is_empty' => false,
  423.                 'extension' => $file->getExtension(),
  424.             ];
  425.         }
  426.         return $arrFileList;
  427.     }
  428.     protected function normalizePath($path)
  429.     {
  430.         return str_replace('\\''/'realpath($path));
  431.     }
  432.     /**
  433.      * @param string $topDir
  434.      */
  435.     protected function checkDir($targetDir$topDir)
  436.     {
  437.         if (strpos($targetDir'..') !== false) {
  438.             return false;
  439.         }
  440.         $targetDir realpath($targetDir);
  441.         $topDir realpath($topDir);
  442.         return strpos($targetDir$topDir) === 0;
  443.     }
  444.     /**
  445.      * @return string
  446.      */
  447.     private function convertStrFromServer($target)
  448.     {
  449.         if ($this->encode == self::SJIS) {
  450.             return mb_convert_encoding($targetself::UTFself::SJIS);
  451.         }
  452.         return $target;
  453.     }
  454.     private function convertStrToServer($target)
  455.     {
  456.         if ($this->encode == self::SJIS) {
  457.             return mb_convert_encoding($targetself::SJISself::UTF);
  458.         }
  459.         return $target;
  460.     }
  461.     private function getUserDataDir($nowDir null)
  462.     {
  463.         return rtrim($this->getParameter('kernel.project_dir').'/html/user_data'.$nowDir'/');
  464.     }
  465.     private function getJailDir($path)
  466.     {
  467.         $realpath realpath($path);
  468.         $jailPath str_replace(realpath($this->getUserDataDir()), ''$realpath);
  469.         return $jailPath $jailPath '/';
  470.     }
  471. }