Completed
Push — master ( 8fe45b...3e4cf7 )
by Jeroen
08:37
created

FileHandler::guessMimeType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 3
cts 4
cp 0.75
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.0625
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
     *
41
     * @var MimeTypeGuesser
42
     */
43
    public $mimeTypeGuesser;
44
45
    /**
46
     * @deprecated This property is deprecated since KunstmaanMediaBundle 5.7 and will be removed in KunstmaanMediaBundle 6.0. Use the `$mimeTypes` property instead.
47
     *
48
     * @var ExtensionGuesser
49
     */
50
    public $extensionGuesser;
51
52
    /** @var MimeTypesInterface */
53
    private $mimeTypes;
54
55
    /**
56
     * Files with a blacklisted extension will be converted to txt
57
     *
58
     * @var array
59
     */
60
    private $blacklistedExtensions = [];
61
62
    /**
63
     * @var SlugifierInterface
64
     */
65
    private $slugifier;
66
67
    /**
68
     * Constructor
69
     *
70
     * @param int                                                $priority
71
     * @param MimeTypeGuesserFactoryInterface|MimeTypesInterface $mimeTypes
72
     * @param ExtensionGuesserFactoryInterface                   $extensionGuesserFactoryInterface
0 ignored issues
show
Documentation introduced by Jeroen Thora
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...
73
     */
74 5
    public function __construct($priority, /*MimeTypesInterface*/ $mimeTypes, ExtensionGuesserFactoryInterface $extensionGuesserFactoryInterface = null)
75
    {
76 5
        parent::__construct($priority);
77
78
        // NEXT_MAJOR: remove type check and enable parameter typehint
79 5
        if (!$mimeTypes instanceof MimeTypesInterface && !$mimeTypes instanceof MimeTypeGuesserFactoryInterface) {
80
            throw new \InvalidArgumentException(sprintf('The "$mimeTypes" argument must implement the "%s" or "%s" interface', MimeTypesInterface::class, MimeTypeGuesserFactoryInterface::class));
81
        }
82
83 5
        if (null !== $extensionGuesserFactoryInterface) {
84
            @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 Jeroen Thora
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...
85
        }
86
87 5
        if ($mimeTypes instanceof MimeTypeGuesserFactoryInterface) {
88
            @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 Jeroen Thora
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...
89
90
            $this->mimeTypeGuesser = $mimeTypes->get();
0 ignored issues
show
Deprecated Code introduced by Jeroen Thora
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...
91
        } else {
92 5
            $this->mimeTypes = $mimeTypes;
93
        }
94
95 5
        if ($extensionGuesserFactoryInterface instanceof ExtensionGuesserFactoryInterface) {
96
            $this->extensionGuesser = $extensionGuesserFactoryInterface->get();
0 ignored issues
show
Deprecated Code introduced by Jeroen Thora
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...
97
        }
98 5
    }
99
100
    /**
101
     * @param SlugifierInterface $slugifier
102
     */
103 1
    public function setSlugifier(SlugifierInterface $slugifier)
104
    {
105 1
        $this->slugifier = $slugifier;
106 1
    }
107
108
    /**
109
     * Inject the blacklisted
110
     *
111
     * @param array $blacklistedExtensions
112
     */
113
    public function setBlacklistedExtensions(array $blacklistedExtensions)
114
    {
115
        $this->blacklistedExtensions = $blacklistedExtensions;
116
    }
117
118
    /**
119
     * Inject the path used in media urls.
120
     *
121
     * @param string $mediaPath
122
     */
123
    public function setMediaPath($mediaPath)
124
    {
125
        $this->mediaPath = $mediaPath;
126
    }
127
128
    public function setFileSystem(Filesystem $fileSystem)
129
    {
130
        $this->fileSystem = $fileSystem;
131
    }
132
133
    /**
134
     * @return string
135
     */
136
    public function getName()
137
    {
138
        return 'File Handler';
139
    }
140
141
    /**
142
     * @return string
143
     */
144
    public function getType()
145
    {
146
        return FileHandler::TYPE;
147
    }
148
149
    /**
150
     * @return string
151
     */
152
    public function getFormType()
153
    {
154
        return FileType::class;
155
    }
156
157
    /**
158
     * @param mixed $object
159
     *
160
     * @return bool
161
     */
162 2
    public function canHandle($object)
163
    {
164 2
        if ($object instanceof File ||
165 2
            ($object instanceof Media &&
166 2
            (is_file($object->getContent()) || $object->getLocation() == 'local'))
167
        ) {
168 1
            return true;
169
        }
170
171 1
        return false;
172
    }
173
174
    /**
175
     * @param Media $media
176
     *
177
     * @return FileHelper
178
     */
179
    public function getFormHelper(Media $media)
180
    {
181
        return new FileHelper($media);
182
    }
183
184
    /**
185
     * @param Media $media
186
     *
187
     * @throws \RuntimeException when the file does not exist
188
     */
189 1
    public function prepareMedia(Media $media)
190
    {
191 1
        if (null === $media->getUuid()) {
192 1
            $uuid = uniqid();
193 1
            $media->setUuid($uuid);
194
        }
195
196 1
        $content = $media->getContent();
197 1
        if (empty($content)) {
198
            return;
199
        }
200
201 1
        if (!$content instanceof File) {
202
            if (!is_file($content)) {
203
                throw new \RuntimeException('Invalid file');
204
            }
205
206
            $file = new File($content);
207
            $media->setContent($file);
208
        }
209
210 1
        $contentType = $this->guessMimeType($content->getPathname());
211 1
        if ($content instanceof UploadedFile) {
212
            $pathInfo = pathinfo($content->getClientOriginalName());
213
214
            if (!\array_key_exists('extension', $pathInfo)) {
215
                $pathInfo['extension'] = $this->getExtensions($contentType);
216
            }
217
218
            $media->setOriginalFilename($this->slugifier->slugify($pathInfo['filename']).'.'.$pathInfo['extension']);
219
            $name = $media->getName();
220
221
            if (empty($name)) {
222
                $media->setName($media->getOriginalFilename());
223
            }
224
        }
225
226 1
        $media->setContentType($contentType);
227 1
        $media->setFileSize(filesize($media->getContent()));
228 1
        $media->setUrl($this->mediaPath.$this->getFilePath($media));
229 1
        $media->setLocation('local');
230 1
    }
231
232
    /**
233
     * @param Media $media
234
     */
235
    public function removeMedia(Media $media)
236
    {
237
        $adapter = $this->fileSystem->getAdapter();
238
239
        // Remove the file from filesystem
240
        $fileKey = $this->getFilePath($media);
241
        if ($adapter->exists($fileKey)) {
242
            $adapter->delete($fileKey);
243
        }
244
245
        // Remove the files containing folder if there's nothing left
246
        $folderPath = $this->getFileFolderPath($media);
247
        if ($adapter->exists($folderPath) && $adapter->isDirectory($folderPath) && !empty($folderPath)) {
248
            $allMyKeys = $adapter->keys();
249
            $everythingfromdir = preg_grep('/'.$folderPath, $allMyKeys);
250
251
            if (\count($everythingfromdir) === 1) {
252
                $adapter->delete($folderPath);
253
            }
254
        }
255
256
        $media->setRemovedFromFileSystem(true);
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262
    public function updateMedia(Media $media)
263
    {
264
        $this->saveMedia($media);
265
    }
266
267
    /**
268
     * @param Media $media
269
     */
270
    public function saveMedia(Media $media)
271
    {
272
        if (!$media->getContent() instanceof File) {
273
            return;
274
        }
275
276
        $originalFile = $this->getOriginalFile($media);
277
        $originalFile->setContent(file_get_contents($media->getContent()->getRealPath()));
278
    }
279
280
    /**
281
     * @param Media $media
282
     *
283
     * @return \Gaufrette\File
284
     */
285
    public function getOriginalFile(Media $media)
286
    {
287
        return $this->fileSystem->get($this->getFilePath($media), true);
288
    }
289
290
    /**
291
     * @param mixed $data
292
     *
293
     * @return Media
0 ignored issues
show
Documentation introduced by Kris Pypen
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...
294
     */
295
    public function createNew($data)
296
    {
297
        if ($data instanceof File) {
298
            /** @var $data File */
299
            $media = new Media();
300
            if (method_exists($data, 'getClientOriginalName')) {
301
                $media->setOriginalFilename($data->getClientOriginalName());
0 ignored issues
show
Bug introduced by Wim Vandersmissen
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...
302
            } else {
303
                $media->setOriginalFilename($data->getFilename());
304
            }
305
            $media->setContent($data);
306
307
            $contentType = $this->guessMimeType($media->getContent()->getPathname());
308
            $media->setContentType($contentType);
309
310
            return $media;
311
        }
312
313
        return null;
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function getShowTemplate(Media $media)
320
    {
321
        return '@KunstmaanMedia/Media/File/show.html.twig';
322
    }
323
324
    /**
325
     * @return array
0 ignored issues
show
Documentation introduced by Kris Pypen
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...
326
     */
327
    public function getAddFolderActions()
328
    {
329
        return [
330
            FileHandler::TYPE => [
331
                'type' => FileHandler::TYPE,
332
                'name' => 'media.file.add',
333
            ],
334
        ];
335
    }
336
337
    /**
338
     * @param Media $media
339
     *
340
     * @return string
341
     */
342 1
    private function getFilePath(Media $media)
343
    {
344 1
        $filename = $media->getOriginalFilename();
345 1
        $filename = str_replace(['/', '\\', '%'], '', $filename);
346
347 1
        if (!empty($this->blacklistedExtensions)) {
348
            $filename = preg_replace('/\.('.implode('|', $this->blacklistedExtensions).')$/', '.txt', $filename);
349
        }
350
351 1
        $parts = pathinfo($filename);
352 1
        $filename = $this->slugifier->slugify($parts['filename']);
353 1
        if (\array_key_exists('extension', $parts)) {
354
            $filename .= '.'.strtolower($parts['extension']);
355
        }
356
357 1
        return sprintf(
358 1
            '%s/%s',
359 1
            $media->getUuid(),
360
            $filename
361
        );
362
    }
363
364
    /**
365
     * @param Media $media
366
     *
367
     * @return string
368
     */
369
    private function getFileFolderPath(Media $media)
370
    {
371
        return substr($this->getFilePath($media), 0, strrpos($this->getFilePath($media), $media->getOriginalFilename()));
372
    }
373
374 1
    private function guessMimeType($pathName)
375
    {
376
        // NEXT_MAJOR: remove method and inline guessMimeType call
377 1
        if ($this->mimeTypeGuesser instanceof MimeTypeGuesser) {
0 ignored issues
show
Deprecated Code introduced by Kevin Jossart
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...
378
            return $this->mimeTypeGuesser->guess($pathName);
0 ignored issues
show
Deprecated Code introduced by Kevin Jossart
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...
379
        }
380
381 1
        return $this->mimeTypes->guessMimeType($pathName);
382
    }
383
384 View Code Duplication
    private function getExtensions($mimeType)
0 ignored issues
show
Duplication introduced by Kevin Jossart
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...
385
    {
386
        // NEXT_MAJOR: remove method and inline getExtensions call
387
        if ($this->extensionGuesser instanceof ExtensionGuesser) {
0 ignored issues
show
Deprecated Code introduced by Kevin Jossart
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...
388
            return $this->extensionGuesser->guess($mimeType);
0 ignored issues
show
Deprecated Code introduced by Kevin Jossart
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...
389
        }
390
391
        return $this->mimeTypes->getExtensions($mimeType)[0] ?? '';
392
    }
393
}
394