Completed
Pull Request — master (#2668)
by Jeroen
06:05
created

FileHandler::getShowTemplate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Kunstmaan\MediaBundle\Helper\File;
4
5
use Gaufrette\Filesystem;
6
use Kunstmaan\MediaBundle\Entity\Media;
7
use Kunstmaan\MediaBundle\Form\File\FileType;
8
use Kunstmaan\MediaBundle\Helper\ExtensionGuesserFactoryInterface;
9
use Kunstmaan\MediaBundle\Helper\Media\AbstractMediaHandler;
10
use Kunstmaan\MediaBundle\Helper\MimeTypeGuesserFactoryInterface;
11
use Kunstmaan\UtilitiesBundle\Helper\SlugifierInterface;
12
use Symfony\Component\HttpFoundation\File\File;
13
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
14
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
15
use Symfony\Component\HttpFoundation\File\UploadedFile;
16
use Symfony\Component\Mime\MimeTypesInterface;
17
18
/**
19
 * FileHandler
20
 */
21
class FileHandler extends AbstractMediaHandler
22
{
23
    /**
24
     * @var string
25
     */
26
    const TYPE = 'file';
27
28
    /**
29
     * @var string
30
     */
31
    public $mediaPath;
32
33
    /**
34
     * @var Filesystem
35
     */
36
    public $fileSystem;
37
38
    /**
39
     * @deprecated This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.
40
     * @var MimeTypeGuesser
41
     */
42
    public $mimeTypeGuesser;
43
44
    /**
45
     * @deprecated This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.
46
     * @var ExtensionGuesser
47
     */
48
    public $extensionGuesser;
49
50
    /** @var MimeTypesInterface */
51
    private $mimeTypes;
52
53
    /**
54
     * Files with a blacklisted extension will be converted to txt
55
     *
56
     * @var array
57
     */
58
    private $blacklistedExtensions = [];
59
60
    /**
61
     * @var SlugifierInterface
62
     */
63
    private $slugifier;
64
65
    /**
66
     * Constructor
67
     *
68
     * @param int                                                $priority
69
     * @param MimeTypeGuesserFactoryInterface|MimeTypesInterface $mimeTypes
70
     * @param ExtensionGuesserFactoryInterface                   $extensionGuesserFactoryInterface
0 ignored issues
show
Documentation introduced by
Should the type for parameter $extensionGuesserFactoryInterface not be null|ExtensionGuesserFactoryInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
71
     */
72 5
    public function __construct($priority, /*MimeTypesInterface*/ $mimeTypes, ExtensionGuesserFactoryInterface $extensionGuesserFactoryInterface = null)
73
    {
74 5
        parent::__construct($priority);
75
76
        // NEXT_MAJOR: remove type check and enable parameter typehint
77 5
        if (!$mimeTypes instanceof MimeTypesInterface && !$mimeTypes instanceof MimeTypeGuesserFactoryInterface) {
78
            throw new \InvalidArgumentException(sprintf('The "$mimeTypes" argument must implement the "%s" or "%s" interface', MimeTypesInterface::class, MimeTypeGuesserFactoryInterface::class));
79
        }
80
81 5
        if (null !== $extensionGuesserFactoryInterface) {
82
            @trigger_error(sprintf('Passing a value for "$extensionGuesserFactoryInterface" in "%s" is deprecated since KunstmaanMediaBundle 5.7 and this parameter will be removed in KunstmaanMediaBundle 6.0.', __METHOD__), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
83
        }
84
85 5
        if ($mimeTypes instanceof MimeTypeGuesserFactoryInterface) {
86
            @trigger_error(sprintf('Passing an instance of "%s" for "$mimeTypes" in "%s" is deprecated since KunstmaanMediaBundle 5.7 and this parameter will be removed in KunstmaanMediaBundle 6.0. Inject the an instance of "%s" instead.', MimeTypeGuesserFactoryInterface::class, __METHOD__, MimeTypesInterface::class), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
87
88
            $this->mimeTypeGuesser = $mimeTypes->get();
0 ignored issues
show
Deprecated Code introduced by
The property Kunstmaan\MediaBundle\He...ndler::$mimeTypeGuesser has been deprecated with message: This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
89
        } else {
90 5
            $this->mimeTypes = $mimeTypes;
91
        }
92
93 5
        if ($extensionGuesserFactoryInterface instanceof ExtensionGuesserFactoryInterface) {
94
            $this->extensionGuesser = $extensionGuesserFactoryInterface->get();
0 ignored issues
show
Deprecated Code introduced by
The property Kunstmaan\MediaBundle\He...dler::$extensionGuesser has been deprecated with message: This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
95
        }
96 5
    }
97
98
    /**
99
     * @param SlugifierInterface $slugifier
100
     */
101 1
    public function setSlugifier(SlugifierInterface $slugifier)
102
    {
103 1
        $this->slugifier = $slugifier;
104 1
    }
105
106
    /**
107
     * Inject the blacklisted
108
     *
109
     * @param array $blacklistedExtensions
110
     */
111
    public function setBlacklistedExtensions(array $blacklistedExtensions)
112
    {
113
        $this->blacklistedExtensions = $blacklistedExtensions;
114
    }
115
116
    /**
117
     * Inject the path used in media urls.
118
     *
119
     * @param string $mediaPath
120
     */
121
    public function setMediaPath($mediaPath)
122
    {
123
        $this->mediaPath = $mediaPath;
124
    }
125
126
    public function setFileSystem(Filesystem $fileSystem)
127
    {
128
        $this->fileSystem = $fileSystem;
129
    }
130
131
    /**
132
     * @return string
133
     */
134
    public function getName()
135
    {
136
        return 'File Handler';
137
    }
138
139
    /**
140
     * @return string
141
     */
142
    public function getType()
143
    {
144
        return FileHandler::TYPE;
145
    }
146
147
    /**
148
     * @return string
149
     */
150
    public function getFormType()
151
    {
152
        return FileType::class;
153
    }
154
155
    /**
156
     * @param mixed $object
157
     *
158
     * @return bool
159
     */
160 2
    public function canHandle($object)
161
    {
162 2
        if ($object instanceof File ||
163 2
            ($object instanceof Media &&
164 2
            (is_file($object->getContent()) || $object->getLocation() == 'local'))
165
        ) {
166 1
            return true;
167
        }
168
169 1
        return false;
170
    }
171
172
    /**
173
     * @param Media $media
174
     *
175
     * @return FileHelper
176
     */
177
    public function getFormHelper(Media $media)
178
    {
179
        return new FileHelper($media);
180
    }
181
182
    /**
183
     * @param Media $media
184
     *
185
     * @throws \RuntimeException when the file does not exist
186
     */
187 1
    public function prepareMedia(Media $media)
188
    {
189 1
        if (null === $media->getUuid()) {
190 1
            $uuid = uniqid();
191 1
            $media->setUuid($uuid);
192
        }
193
194 1
        $content = $media->getContent();
195 1
        if (empty($content)) {
196
            return;
197
        }
198
199 1
        if (!$content instanceof File) {
200
            if (!is_file($content)) {
201
                throw new \RuntimeException('Invalid file');
202
            }
203
204
            $file = new File($content);
205
            $media->setContent($file);
206
        }
207
208 1
        $contentType = $this->guessMimeType($content->getPathname());
209 1
        if ($content instanceof UploadedFile) {
210
            $pathInfo = pathinfo($content->getClientOriginalName());
211
212
            if (!\array_key_exists('extension', $pathInfo)) {
213
                $pathInfo['extension'] = $this->getExtensions($contentType);
214
            }
215
216
            $media->setOriginalFilename($this->slugifier->slugify($pathInfo['filename']).'.'.$pathInfo['extension']);
217
            $name = $media->getName();
218
219
            if (empty($name)) {
220
                $media->setName($media->getOriginalFilename());
221
            }
222
        }
223
224 1
        $media->setContentType($contentType);
225 1
        $media->setFileSize(filesize($media->getContent()));
226 1
        $media->setUrl($this->mediaPath.$this->getFilePath($media));
227 1
        $media->setLocation('local');
228 1
    }
229
230
    /**
231
     * @param Media $media
232
     */
233
    public function removeMedia(Media $media)
234
    {
235
        $adapter = $this->fileSystem->getAdapter();
236
237
        // Remove the file from filesystem
238
        $fileKey = $this->getFilePath($media);
239
        if ($adapter->exists($fileKey)) {
240
            $adapter->delete($fileKey);
241
        }
242
243
        // Remove the files containing folder if there's nothing left
244
        $folderPath = $this->getFileFolderPath($media);
245
        if ($adapter->exists($folderPath) && $adapter->isDirectory($folderPath) && !empty($folderPath)) {
246
            $allMyKeys = $adapter->keys();
247
            $everythingfromdir = preg_grep('/'.$folderPath, $allMyKeys);
248
249
            if (\count($everythingfromdir) === 1) {
250
                $adapter->delete($folderPath);
251
            }
252
        }
253
254
        $media->setRemovedFromFileSystem(true);
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function updateMedia(Media $media)
261
    {
262
        $this->saveMedia($media);
263
    }
264
265
    /**
266
     * @param Media $media
267
     */
268
    public function saveMedia(Media $media)
269
    {
270
        if (!$media->getContent() instanceof File) {
271
            return;
272
        }
273
274
        $originalFile = $this->getOriginalFile($media);
275
        $originalFile->setContent(file_get_contents($media->getContent()->getRealPath()));
276
    }
277
278
    /**
279
     * @param Media $media
280
     *
281
     * @return \Gaufrette\File
282
     */
283
    public function getOriginalFile(Media $media)
284
    {
285
        return $this->fileSystem->get($this->getFilePath($media), true);
286
    }
287
288
    /**
289
     * @param mixed $data
290
     *
291
     * @return Media
0 ignored issues
show
Documentation introduced by
Should the return type not be Media|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
292
     */
293
    public function createNew($data)
294
    {
295
        if ($data instanceof File) {
296
            /** @var $data File */
297
            $media = new Media();
298
            if (method_exists($data, 'getClientOriginalName')) {
299
                $media->setOriginalFilename($data->getClientOriginalName());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\HttpFoundation\File\File as the method getClientOriginalName() does only exist in the following sub-classes of Symfony\Component\HttpFoundation\File\File: Symfony\Component\HttpFoundation\File\UploadedFile. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
300
            } else {
301
                $media->setOriginalFilename($data->getFilename());
302
            }
303
            $media->setContent($data);
304
305
            $contentType = $this->guessMimeType($media->getContent()->getPathname());
306
            $media->setContentType($contentType);
307
308
            return $media;
309
        }
310
311
        return null;
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function getShowTemplate(Media $media)
318
    {
319
        return '@KunstmaanMedia/Media/File/show.html.twig';
320
    }
321
322
    /**
323
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<*,array<string,string>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
324
     */
325
    public function getAddFolderActions()
326
    {
327
        return [
328
            FileHandler::TYPE => [
329
                'type' => FileHandler::TYPE,
330
                'name' => 'media.file.add',
331
            ],
332
        ];
333
    }
334
335
    /**
336
     * @param Media $media
337
     *
338
     * @return string
339
     */
340 1
    private function getFilePath(Media $media)
341
    {
342 1
        $filename = $media->getOriginalFilename();
343 1
        $filename = str_replace(['/', '\\', '%'], '', $filename);
344
345 1
        if (!empty($this->blacklistedExtensions)) {
346
            $filename = preg_replace('/\.('.implode('|', $this->blacklistedExtensions).')$/', '.txt', $filename);
347
        }
348
349 1
        $parts = pathinfo($filename);
350 1
        $filename = $this->slugifier->slugify($parts['filename']);
351 1
        if (\array_key_exists('extension', $parts)) {
352
            $filename .= '.'.strtolower($parts['extension']);
353
        }
354
355 1
        return sprintf(
356 1
            '%s/%s',
357 1
            $media->getUuid(),
358
            $filename
359
        );
360
    }
361
362
    /**
363
     * @param Media $media
364
     *
365
     * @return string
366
     */
367
    private function getFileFolderPath(Media $media)
368
    {
369
        return substr($this->getFilePath($media), 0, strrpos($this->getFilePath($media), $media->getOriginalFilename()));
370
    }
371
372 1
    private function guessMimeType($pathName)
373
    {
374
        // NEXT_MAJOR: remove method and inline guessMimeType call
375 1
        if ($this->mimeTypeGuesser instanceof MimeTypeGuesser) {
0 ignored issues
show
Deprecated Code introduced by
The property Kunstmaan\MediaBundle\He...ndler::$mimeTypeGuesser has been deprecated with message: This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
376
            return $this->mimeTypeGuesser->guess($pathName);
0 ignored issues
show
Deprecated Code introduced by
The property Kunstmaan\MediaBundle\He...ndler::$mimeTypeGuesser has been deprecated with message: This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
377
        }
378
379 1
        return $this->mimeTypes->guessMimeType($pathName);
380
    }
381
382 View Code Duplication
    private function getExtensions($mimeType)
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...
383
    {
384
        // NEXT_MAJOR: remove method and inline getExtensions call
385
        if ($this->extensionGuesser instanceof ExtensionGuesser) {
0 ignored issues
show
Deprecated Code introduced by
The property Kunstmaan\MediaBundle\He...dler::$extensionGuesser has been deprecated with message: This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
386
            return $this->extensionGuesser->guess($mimeType);
0 ignored issues
show
Deprecated Code introduced by
The property Kunstmaan\MediaBundle\He...dler::$extensionGuesser has been deprecated with message: This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
387
        }
388
389
        return $this->mimeTypes->getExtensions($mimeType)[0] ?? '';
390
    }
391
}
392