Completed
Pull Request — master (#2668)
by Jeroen
44:46 queued 38:10
created

FileHandler::removeMedia()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 0
cts 13
cp 0
rs 8.9297
c 0
b 0
f 0
cc 6
nc 6
nop 1
crap 42
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\MimeTypes;
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
     * @var MimeTypeGuesser
40
     */
41
    public $mimeTypeGuesser;
42
43
    /**
44
     * @var ExtensionGuesser
45
     */
46
    public $extensionGuesser;
47
48
    /**
49
     * Files with a blacklisted extension will be converted to txt
50
     *
51
     * @var array
52
     */
53
    private $blacklistedExtensions = [];
54
55
    /**
56
     * @var SlugifierInterface
57
     */
58
    private $slugifier;
59
60
    /**
61
     * Constructor
62
     *
63
     * @param int                                   $priority
64
     * @param MimeTypeGuesserFactoryInterface       $mimeTypeGuesserFactory
65
     * @param ExtensionGuesserFactoryInterface|null $extensionGuesserFactoryInterface
66
     */
67 5
    public function __construct($priority, $mimeTypeGuesserFactory, $extensionGuesserFactoryInterface, MimeTypes $mimeTypes = null)
68
    {
69 5
        parent::__construct($priority);
70 5
        $this->mimeTypeGuesser = $mimeTypeGuesserFactory->get();
71 1
        $this->extensionGuesser = $extensionGuesserFactoryInterface->get();
0 ignored issues
show
Bug introduced by
It seems like $extensionGuesserFactoryInterface is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
72 1
    }
73
74
    /**
75
     * @param SlugifierInterface $slugifier
76
     */
77 1
    public function setSlugifier(SlugifierInterface $slugifier)
78
    {
79 1
        $this->slugifier = $slugifier;
80 1
    }
81
82
    /**
83
     * Inject the blacklisted
84
     *
85
     * @param array $blacklistedExtensions
86
     */
87
    public function setBlacklistedExtensions(array $blacklistedExtensions)
88
    {
89
        $this->blacklistedExtensions = $blacklistedExtensions;
90
    }
91
92
    /**
93
     * Inject the path used in media urls.
94
     *
95
     * @param string $mediaPath
96
     */
97
    public function setMediaPath($mediaPath)
98
    {
99
        $this->mediaPath = $mediaPath;
100
    }
101
102
    public function setFileSystem(Filesystem $fileSystem)
103
    {
104
        $this->fileSystem = $fileSystem;
105
    }
106
107
    /**
108
     * @return string
109
     */
110
    public function getName()
111
    {
112
        return 'File Handler';
113
    }
114
115
    /**
116
     * @return string
117
     */
118
    public function getType()
119
    {
120
        return FileHandler::TYPE;
121
    }
122
123
    /**
124
     * @return string
125
     */
126
    public function getFormType()
127
    {
128
        return FileType::class;
129
    }
130
131
    /**
132
     * @param mixed $object
133
     *
134
     * @return bool
135
     */
136
    public function canHandle($object)
137
    {
138
        if ($object instanceof File ||
139
            ($object instanceof Media &&
140
            (is_file($object->getContent()) || $object->getLocation() == 'local'))
141
        ) {
142
            return true;
143
        }
144
145
        return false;
146
    }
147
148
    /**
149
     * @param Media $media
150
     *
151
     * @return FileHelper
152
     */
153
    public function getFormHelper(Media $media)
154
    {
155
        return new FileHelper($media);
156
    }
157
158
    /**
159
     * @param Media $media
160
     *
161
     * @throws \RuntimeException when the file does not exist
162
     */
163 1
    public function prepareMedia(Media $media)
164
    {
165 1
        if (null === $media->getUuid()) {
166 1
            $uuid = uniqid();
167 1
            $media->setUuid($uuid);
168
        }
169
170 1
        $content = $media->getContent();
171 1
        if (empty($content)) {
172
            return;
173
        }
174
175 1
        if (!$content instanceof File) {
176
            if (!is_file($content)) {
177
                throw new \RuntimeException('Invalid file');
178
            }
179
180
            $file = new File($content);
181
            $media->setContent($file);
182
        }
183
184 1
        $contentType = $this->guessMimeType($content->getPathname());
185 1
        if ($content instanceof UploadedFile) {
186
            $pathInfo = pathinfo($content->getClientOriginalName());
187
188
            if (!\array_key_exists('extension', $pathInfo)) {
189
                $pathInfo['extension'] = $this->getExtensions($contentType);
190
            }
191
192
            $media->setOriginalFilename($this->slugifier->slugify($pathInfo['filename']).'.'.$pathInfo['extension']);
193
            $name = $media->getName();
194
195
            if (empty($name)) {
196
                $media->setName($media->getOriginalFilename());
197
            }
198
        }
199
200 1
        $media->setContentType($contentType);
201 1
        $media->setFileSize(filesize($media->getContent()));
202 1
        $media->setUrl($this->mediaPath.$this->getFilePath($media));
203 1
        $media->setLocation('local');
204 1
    }
205
206
    /**
207
     * @param Media $media
208
     */
209
    public function removeMedia(Media $media)
210
    {
211
        $adapter = $this->fileSystem->getAdapter();
212
213
        // Remove the file from filesystem
214
        $fileKey = $this->getFilePath($media);
215
        if ($adapter->exists($fileKey)) {
216
            $adapter->delete($fileKey);
217
        }
218
219
        // Remove the files containing folder if there's nothing left
220
        $folderPath = $this->getFileFolderPath($media);
221
        if ($adapter->exists($folderPath) && $adapter->isDirectory($folderPath) && !empty($folderPath)) {
222
            $allMyKeys = $adapter->keys();
223
            $everythingfromdir = preg_grep('/'.$folderPath, $allMyKeys);
224
225
            if (\count($everythingfromdir) === 1) {
226
                $adapter->delete($folderPath);
227
            }
228
        }
229
230
        $media->setRemovedFromFileSystem(true);
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236
    public function updateMedia(Media $media)
237
    {
238
        $this->saveMedia($media);
239
    }
240
241
    /**
242
     * @param Media $media
243
     */
244
    public function saveMedia(Media $media)
245
    {
246
        if (!$media->getContent() instanceof File) {
247
            return;
248
        }
249
250
        $originalFile = $this->getOriginalFile($media);
251
        $originalFile->setContent(file_get_contents($media->getContent()->getRealPath()));
252
    }
253
254
    /**
255
     * @param Media $media
256
     *
257
     * @return \Gaufrette\File
258
     */
259
    public function getOriginalFile(Media $media)
260
    {
261
        return $this->fileSystem->get($this->getFilePath($media), true);
262
    }
263
264
    /**
265
     * @param mixed $data
266
     *
267
     * @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...
268
     */
269
    public function createNew($data)
270
    {
271
        if ($data instanceof File) {
272
            /** @var $data File */
273
            $media = new Media();
274
            if (method_exists($data, 'getClientOriginalName')) {
275
                $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...
276
            } else {
277
                $media->setOriginalFilename($data->getFilename());
278
            }
279
            $media->setContent($data);
280
281
            $contentType = $this->guessMimeType($media->getContent()->getPathname());
282
            $media->setContentType($contentType);
283
284
            return $media;
285
        }
286
287
        return null;
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    public function getShowTemplate(Media $media)
294
    {
295
        return '@KunstmaanMedia/Media/File/show.html.twig';
296
    }
297
298
    /**
299
     * @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...
300
     */
301
    public function getAddFolderActions()
302
    {
303
        return [
304
            FileHandler::TYPE => [
305
                'type' => FileHandler::TYPE,
306
                'name' => 'media.file.add',
307
            ],
308
        ];
309
    }
310
311
    /**
312
     * @param Media $media
313
     *
314
     * @return string
315
     */
316 1
    private function getFilePath(Media $media)
317
    {
318 1
        $filename = $media->getOriginalFilename();
319 1
        $filename = str_replace(['/', '\\', '%'], '', $filename);
320
321 1
        if (!empty($this->blacklistedExtensions)) {
322
            $filename = preg_replace('/\.('.implode('|', $this->blacklistedExtensions).')$/', '.txt', $filename);
323
        }
324
325 1
        $parts = pathinfo($filename);
326 1
        $filename = $this->slugifier->slugify($parts['filename']);
327 1
        if (\array_key_exists('extension', $parts)) {
328
            $filename .= '.'.strtolower($parts['extension']);
329
        }
330
331 1
        return sprintf(
332 1
            '%s/%s',
333 1
            $media->getUuid(),
334
            $filename
335
        );
336
    }
337
338
    /**
339
     * @param Media $media
340
     *
341
     * @return string
342
     */
343
    private function getFileFolderPath(Media $media)
344
    {
345
        return substr($this->getFilePath($media), 0, strrpos($this->getFilePath($media), $media->getOriginalFilename()));
346
    }
347
348 1
    private function guessMimeType($pathName)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
349
    {
350 1
        if ($this->mimeTypeGuesser instanceof MimeTypeGuesser) {
351 1
            return $this->mimeTypeGuesser->guess($pathName);
352
        }
353
354
        return $this->mimeTypeGuesser->guessMimeType($pathName);
355
    }
356
357 View Code Duplication
    private function getExtensions($mimeType)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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...
358
    {
359
        if ($this->extensionGuesser instanceof ExtensionGuesser) {
360
            return $this->extensionGuesser->guess($mimeType);
361
        }
362
363
        return $this->extensionGuesser->getExtensions($mimeType)[0] ?? '';
364
    }
365
}
366