Failed Conditions
Pull Request — 4.0 (#4836)
by
unknown
05:00
created

FileController::checkFileExistDir()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 3
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.ec-cube.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Controller\Admin\Content;
15
16
use Eccube\Controller\AbstractController;
17
use Eccube\Util\FilesystemUtil;
18
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
19
use Symfony\Component\Filesystem\Exception\IOException;
20
use Symfony\Component\Filesystem\Filesystem;
21
use Symfony\Component\Finder\Finder;
22
use Symfony\Component\Form\Extension\Core\Type\FileType;
23
use Symfony\Component\Form\Extension\Core\Type\FormType;
24
use Symfony\Component\Form\Extension\Core\Type\TextType;
25
use Symfony\Component\HttpFoundation\BinaryFileResponse;
26
use Symfony\Component\HttpFoundation\File\Exception\FileException;
27
use Symfony\Component\HttpFoundation\Request;
28
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
29
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
30
use Symfony\Component\Routing\Annotation\Route;
31
use Symfony\Component\Validator\Constraints as Assert;
32
33
class FileController extends AbstractController
34
{
35
    const SJIS = 'sjis-win';
36
    const UTF = 'UTF-8';
37
    private $errors = [];
38
    private $encode = '';
39
40
    /**
41
     * FileController constructor.
42
     */
43
    public function __construct()
44 6
    {
45
        $this->encode = self::UTF;
46 6
        if ('\\' === DIRECTORY_SEPARATOR) {
47 6
            $this->encode = self::SJIS;
48
        }
49
    }
50
51
    /**
52
     * @Route("/%eccube_admin_route%/content/file_manager", name="admin_content_file")
53
     * @Template("@admin/Content/file.twig")
54
     */
55
    public function index(Request $request)
56 3
    {
57
        $form = $this->formFactory->createBuilder(FormType::class)
58 3
            ->add('file', FileType::class, [
59 3
                'multiple' => true,
60 3
                'attr' => [
61 3
                    'multiple' => 'multiple'
62
                ],
63
            ])
64 3
            ->add('create_file', TextType::class)
65 3
            ->getForm();
66
67
        // user_data_dir
68 3
        $userDataDir = $this->getUserDataDir();
69
        $topDir = $this->normalizePath($userDataDir);
70
        //        $topDir = '/';
71 3
        // user_data_dirの親ディレクトリ
72 3
        $htmlDir = $this->normalizePath($this->getUserDataDir().'/../');
73 3
74
        // カレントディレクトリ
75
        $nowDir = $this->checkDir($this->getUserDataDir($request->get('tree_select_file')), $this->getUserDataDir())
76 3
            ? $this->normalizePath($this->getUserDataDir($request->get('tree_select_file')))
77 3
            : $topDir;
78 3
79 3
        // パンくず表示用データ
80
        $nowDirList = json_encode(explode('/', trim(str_replace($htmlDir, '', $nowDir), '/')));
81 3
        $jailNowDir = $this->getJailDir($nowDir);
82 2
        $isTopDir = ($topDir === $jailNowDir);
83
        $parentDir = substr($nowDir, 0, strrpos($nowDir, '/'));
84 1
85 1
        if ('POST' === $request->getMethod()) {
86
            switch ($request->get('mode')) {
87 1
                case 'create':
88 1
                    $this->create($request);
89
                    break;
90
                case 'upload':
91
                    $this->upload($request);
92
                    break;
93 3
                default:
94 3
                    break;
95 3
            }
96 3
        }
97
        $tree = $this->getTree($this->getUserDataDir(), $request);
98
        $arrFileList = $this->getFileList($nowDir);
99 3
        $paths = $this->getPathsToArray($tree);
100 3
        $tree = $this->getTreeToArray($tree);
101 3
102 3
        return [
103 3
            'form' => $form->createView(),
104 3
            'tpl_javascript' => json_encode($tree),
105 3
            'top_dir' => $this->getJailDir($topDir),
106 3
            'tpl_is_top_dir' => $isTopDir,
107 3
            'tpl_now_dir' => $jailNowDir,
108 3
            'html_dir' => $this->getJailDir($htmlDir),
109 3
            'now_dir_list' => $nowDirList,
110
            'tpl_parent_dir' => $this->getJailDir($parentDir),
111
            'arrFileList' => $arrFileList,
112
            'errors' => $this->errors,
113
            'paths' => json_encode($paths),
114
        ];
115
    }
116 1
117
    /**
118 1
     * @Route("/%eccube_admin_route%/content/file_view", name="admin_content_file_view")
119 1
     */
120 1
    public function view(Request $request)
121
    {
122 1
        $file = $this->convertStrToServer($this->getUserDataDir($request->get('file')));
123
        if ($this->checkDir($file, $this->getUserDataDir())) {
124
            setlocale(LC_ALL, 'ja_JP.UTF-8');
125
126
            return new BinaryFileResponse($file);
127
        }
128
129
        throw new NotFoundHttpException();
130
    }
131
132
    /**
133 1
     * Create directory
134
     *
135 1
     * @param Request $request
136 1
     */
137 1
    public function create(Request $request)
138
    {
139 1
        $form = $this->formFactory->createBuilder(FormType::class)
140 1
            ->add('file', FileType::class, [
141
                'multiple' => true,
142 1
                'attr' => [
143 1
                    'multiple' => 'multiple'
144
                ],
145
            ])
146
            ->add('create_file', TextType::class, [
147 1
                'constraints' => [
148 1
                    new Assert\NotBlank(),
149
                    new Assert\Regex([
150
                        'pattern' => '/[^[:alnum:]_.\\-]/',
151
                        'match' => false,
152
                        'message' => 'file.text.error.folder_symbol',
153
                    ]),
154 1
                    new Assert\Regex([
155
                        'pattern' => "/^\.(.*)$/",
156 1
                        'match' => false,
157 1
                        'message' => 'file.text.error.folder_period',
158
                    ]),
159
                ],
160
            ])
161
            ->getForm();
162
163
        $form->handleRequest($request);
164 View Code Duplication
        if (!$form->isValid()) {
165 1
            foreach ($form->getErrors(true) as $error) {
166 1
                $this->errors[] = ['message' => $error->getMessage()];
0 ignored issues
show
Bug introduced by
The method getMessage does only exist in Symfony\Component\Form\FormError, but not in Symfony\Component\Form\FormErrorIterator.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
167
            }
168
169 1
            return;
170 1
        }
171 1
172
        $fs = new Filesystem();
173 1
        $filename = $form->get('create_file')->getData();
174 1
175
        $topDir = $this->getUserDataDir();
176 1
        $nowDir = $this->getUserDataDir($request->get('now_dir'));
177
        $nowDir = $this->checkDir($nowDir, $topDir)
178
            ? $this->normalizePath($nowDir)
179
            : $topDir;
180
        if ($this->checkFileExistDir($nowDir, $filename)) {
181
            $this->errors[] = ['message' => 'admin.content.file.dir_exists'];
182
            return;
183
        }
184
185
        try {
186 1
            $fs->mkdir($nowDir.'/'.$filename);
187
            $this->addSuccess('admin.common.create_complete', 'admin');
188 1
        } catch (IOException $e) {
189
            $this->errors[] = ['message' => $e->getMessage()];
190 1
        }
191 1
    }
192 1
193 1
    /**
194 1
     * @Route("/%eccube_admin_route%/content/file_delete", name="admin_content_file_delete", methods={"DELETE"})
195 1
     */
196 1
    public function delete(Request $request)
197
    {
198
        $this->isTokenValid();
199
200 1
        $selectFile = $request->get('select_file');
201
        if (is_null($selectFile) || $selectFile == '/') {
202
            return $this->redirectToRoute('admin_content_file');
203
        }
204
205
        $topDir = $this->getUserDataDir();
206 1
        $file = $this->convertStrToServer($this->getUserDataDir($selectFile));
207
        if ($this->checkDir($file, $topDir)) {
208 1
            $fs = new Filesystem();
209 1
            if ($fs->exists($file)) {
210 1
                $fs->remove($file);
211 1
                $this->addSuccess('admin.common.delete_complete', 'admin');
212 1
            }
213 1
        }
214
215
        // 削除実行時のカレントディレクトリを表示させる
216 1
        return $this->redirectToRoute('admin_content_file', array('tree_select_file' => dirname($selectFile)));
217
    }
218
219
    /**
220
     * @Route("/%eccube_admin_route%/content/file_download", name="admin_content_file_download")
221 1
     */
222 1
    public function download(Request $request)
223 1
    {
224
        $topDir = $this->getUserDataDir();
225
        $file = $this->convertStrToServer($this->getUserDataDir($request->get('select_file')));
226
        if ($this->checkDir($file, $topDir)) {
227
            if (!is_dir($file)) {
228
                setlocale(LC_ALL, 'ja_JP.UTF-8');
229
                $pathParts = pathinfo($file);
230
231
                $patterns = [
232
                    '/[a-zA-Z0-9!"#$%&()=~^|@`:*;+{}]/',
233
                    '/[- ,.<>?_[\]\/\\\\]/',
234
                    "/['\r\n\t\v\f]/",
235 1
                ];
236
237 1
                $str = preg_replace($patterns, '', $pathParts['basename']);
238 1
                if (strlen($str) === 0) {
239
                    return (new BinaryFileResponse($file))->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT);
240 1
                } else {
241 1
                    return new BinaryFileResponse($file, 200, [
242
                        'Content-Type' => 'aplication/octet-stream;',
243
                        'Content-Disposition' => "attachment; filename*=UTF-8\'\'".rawurlencode($this->convertStrFromServer($pathParts['basename'])),
244
                    ]);
245 1
                }
246 1
            }
247
        }
248 1
        throw new NotFoundHttpException();
249
    }
250 1
251
    public function upload(Request $request)
252
    {
253
        $form = $this->formFactory->createBuilder(FormType::class)
254
            ->add('file', FileType::class, [
255
                'multiple' => true,
256
                'constraints' => [
257
                    new Assert\NotBlank([
258 1
                        'message' => 'admin.common.file_select_empty',
259 1
                    ]),
260 1
                ],
261
            ])
262 1
            ->add('create_file', TextType::class)
263
            ->getForm();
264
265
        $form->handleRequest($request);
266
267 View Code Duplication
        if (!$form->isValid()) {
268 1
            foreach ($form->getErrors(true) as $error) {
269
                $this->errors[] = ['message' => $error->getMessage()];
0 ignored issues
show
Bug introduced by
The method getMessage does only exist in Symfony\Component\Form\FormError, but not in Symfony\Component\Form\FormErrorIterator.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
270 1
            }
271 1
272
            return;
273
        }
274
275
        $data = $form->getData();
276
        $topDir = $this->getUserDataDir();
277 3
        $nowDir = $this->getUserDataDir($request->get('now_dir'));
278
279 3
        if (!$this->checkDir($nowDir, $topDir)) {
280 3
            $this->errors[] = ['message' => 'file.text.error.invalid_upload_folder'];
281 3
            return;
282 3
        }
283 3
284 3
        $uploadCount = count($data['file']);
285 3
        $successCount = 0;
286 3
287 3
        foreach ($data['file'] as $file) {
288
            $filename = $this->convertStrToServer($file->getClientOriginalName());
289
            try {
290
                $file->move($nowDir, $filename);
291 3
                $successCount ++;
292
            } catch (FileException $e) {
293
                $this->errors[] = ['message' => trans('admin.content.file.upload_error', [
294 3
                    '%file_name%' => $filename,
295
                    '%error%' => $e->getMessage()
296 3
                ])];
297 3
            }
298 3
        }
299
        if ($successCount > 0) {
300
            $this->addSuccess(trans('admin.content.file.upload_complete', [
301 3
                '%success%' => $successCount,
302
                '%count%' => $uploadCount
303
            ]), 'admin');
304
        }
305
    }
306
307
    private function getTreeToArray($tree)
308 3
    {
309
        $arrTree = [];
310 3
        foreach ($tree as $key => $val) {
311 3
            $path = $this->getJailDir($val['path']);
312 3
            $arrTree[$key] = [
313
                $key,
314 3
                $val['type'],
315 3
                $path,
316 3
                $val['depth'],
317 3
                $val['open'] ? 'true' : 'false',
318 3
            ];
319
        }
320
321
        return $arrTree;
322 3
    }
323
324 3
    private function getPathsToArray($tree)
325 3
    {
326
        $paths = [];
327
        foreach ($tree as $val) {
328
            $paths[] = $this->getJailDir($val['path']);
329 3
        }
330 1
331 1
        return $paths;
332 1
    }
333 1
334 1
    /**
335 1
     * @param string $topDir
336 1
     * @param Request $request
337 1
     */
338
    private function getTree($topDir, $request)
339
    {
340
        $finder = Finder::create()->in($topDir)
341 3
            ->directories()
342
            ->sortByName();
343
344
        $tree = [];
345
        $tree[] = [
346
            'path' => $topDir,
347 3
            'type' => '_parent',
348
            'depth' => 0,
349 3
            'open' => true,
350 3
        ];
351 3
352 3
        $defaultDepth = count(explode('/', $topDir));
353
354 3
        $openDirs = [];
355 3
        if ($request->get('tree_status')) {
356
            $openDirs = explode('|', $request->get('tree_status'));
357 3
        }
358 3
359 3
        foreach ($finder as $dirs) {
360 3
            $path = $this->normalizePath($dirs->getRealPath());
361 3
            $type = (iterator_count(Finder::create()->in($path)->directories())) ? '_parent' : '_child';
362 3
            $depth = count(explode('/', $path)) - $defaultDepth;
363 3
            $tree[] = [
364
                'path' => $path,
365 3
                'type' => $type,
366
                'depth' => $depth,
367
                'open' => (in_array($path, $openDirs)) ? true : false,
368
            ];
369
        }
370 3
371
        return $tree;
372 3
    }
373
374
    /**
375
     * @param string $nowDir
376
     */
377 3
    private function getFileList($nowDir)
378 3
    {
379 1
        $topDir = $this->getuserDataDir();
380 1
        $filter = function (\SplFileInfo $file) use ($topDir) {
381 1
            $acceptPath = realpath($topDir);
382 1
            $targetPath = $file->getRealPath();
383 1
384 1
            return strpos($targetPath, $acceptPath) === 0;
385 1
        };
386 1
387 1
        $finder = Finder::create()
388 1
            ->filter($filter)
389 1
            ->in($nowDir)
390 1
            ->ignoreDotFiles(false)
391 1
            ->sortByName()
392 1
            ->depth(0);
393 1
        $dirFinder = $finder->directories();
394 1
        try {
395 1
            $dirs = $dirFinder->getIterator();
396
        } catch (\Exception $e) {
397 1
            $dirs = [];
398
        }
399
400 3
        $fileFinder = $finder->files();
401 3
        try {
402 3
            $files = $fileFinder->getIterator();
403 3
        } catch (\Exception $e) {
404 3
            $files = [];
405 3
        }
406
407
        $arrFileList = [];
408 3
        foreach ($dirs as $dir) {
409
            $dirPath = $this->normalizePath($dir->getRealPath());
410
            $childDir = Finder::create()
411
                ->in($dirPath)
412 3
                ->ignoreDotFiles(false)
413
                ->directories()
414
                ->depth(0);
415 3
            $childFile = Finder::create()
416
                ->in($dirPath)
417 3
                ->ignoreDotFiles(false)
418
                ->files()
419
                ->depth(0);
420
            $countNumber = $childDir->count() + $childFile->count();
421
            $arrFileList[] = [
422
                'file_name' => $this->convertStrFromServer($dir->getFilename()),
423 6
                'file_path' => $this->convertStrFromServer($this->getJailDir($dirPath)),
424
                'file_size' => FilesystemUtil::sizeToHumanReadable($dir->getSize()),
425 6
                'file_time' => $dir->getmTime(),
426 6
                'is_dir' => true,
427
                'is_empty' => $countNumber == 0 ? true : false,
428 6
            ];
429
        }
430
        foreach ($files as $file) {
431
            $arrFileList[] = [
432
                'file_name' => $this->convertStrFromServer($file->getFilename()),
433
                'file_path' => $this->convertStrFromServer($this->getJailDir($this->normalizePath($file->getRealPath()))),
434 3
                'file_size' => FilesystemUtil::sizeToHumanReadable($file->getSize()),
435
                'file_time' => $file->getmTime(),
436 3
                'is_dir' => false,
437
                'is_empty' => false,
438
                'extension' => $file->getExtension(),
439
            ];
440 3
        }
441
442
        return $arrFileList;
443 4
    }
444
445 4
    /**
446
     * 指定のディレクトリに指定の名前のファイルが存在するか調べる
447
     * @param string $dir
448
     * @param string $filename
449 4
     * @return bool
450
     */
451
    private function checkFileExistDir(string $dir, string $filename): bool
452 6
    {
453
        $file_list = $this->getFileList($dir);
454 6
        foreach ($file_list as $file) {
455
            if ($file['file_name'] === $filename) {
456
                return true;
457 3
            }
458
        }
459 3
        return false;
460 3
    }
461
462 3
    protected function normalizePath($path)
463
    {
464
        return str_replace('\\', '/', realpath($path));
465
    }
466
467
    /**
468
     * @param string $topDir
469
     */
470
    protected function checkDir($targetDir, $topDir)
471
    {
472
        $targetDir = realpath($targetDir);
473
        $topDir = realpath($topDir);
474
475
        return strpos($targetDir, $topDir) === 0;
476
    }
477
478
    /**
479
     * @return string
480
     */
481 View Code Duplication
    private function convertStrFromServer($target)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
482
    {
483
        if ($this->encode == self::SJIS) {
484
            return mb_convert_encoding($target, self::UTF, self::SJIS);
485
        }
486
487
        return $target;
488
    }
489
490 View Code Duplication
    private function convertStrToServer($target)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
491
    {
492
        if ($this->encode == self::SJIS) {
493
            return mb_convert_encoding($target, self::SJIS, self::UTF);
494
        }
495
496
        return $target;
497
    }
498
499
    private function getUserDataDir($nowDir = null)
500
    {
501
        return rtrim($this->getParameter('kernel.project_dir').'/html/user_data'.$nowDir, '/');
502
    }
503
504
    private function getJailDir($path)
505
    {
506
        $realpath = realpath($path);
507
        $jailPath = str_replace(realpath($this->getUserDataDir()), '', $realpath);
508
509
        return $jailPath ? $jailPath : '/';
510
    }
511
}
512