MediaManager::cleanup()   B
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
cc 5
eloc 19
nc 8
nop 0
1
<?php
2
3
namespace Alpixel\Bundle\MediaBundle\Services;
4
5
use Alpixel\Bundle\MediaBundle\Entity\Media;
6
use Alpixel\Bundle\MediaBundle\Exception\InvalidMimeTypeException;
7
use Cocur\Slugify\Slugify;
8
use Doctrine\ORM\EntityManager;
9
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
10
use Symfony\Component\Filesystem\Exception\IOException;
11
use Symfony\Component\Filesystem\Filesystem;
12
use Symfony\Component\Finder\Finder;
13
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
14
use Symfony\Component\HttpFoundation\File\Exception\UploadException;
15
use Symfony\Component\HttpFoundation\File\File;
16
use Symfony\Component\HttpFoundation\File\UploadedFile;
17
use Symfony\Component\Routing\Generator\UrlGenerator;
18
19
/**
20
 * @author Benjamin HUBERT <[email protected]>
21
 */
22
class MediaManager
23
{
24
    /**
25
     * @var \Doctrine\ORM\EntityManager
26
     */
27
    protected $entityManager;
28
    protected $uploadDir;
29
    protected $uploadConfiguration;
30
31
    use ContainerAwareTrait;
32
33
    const SIZE_OF_KIBIOCTET = 1024;
34
    const OCTET_IN_KO = 1;
35
    const OCTET_IN_MO = 2;
36
    const OCTET_IN_GO = 3;
37
    const OCTET_IN_TO = 4;
38
    const OCTET_IN_PO = 5;
39
40
    public function __construct(EntityManager $entityManager, $uploadDir, $uploadConfiguration)
41
    {
42
        $this->entityManager = $entityManager;
43
        if (substr($uploadDir, -1) !== '/') {
44
            $uploadDir = $uploadDir.'/';
45
        }
46
        $this->uploadDir = $uploadDir;
47
        $this->uploadConfiguration = $uploadConfiguration;
48
    }
49
50
51
    /**
52
     * @param \Symfony\Component\HttpFoundation\File\File $file
53
     * @param string $dest_folder
54
     * @param \DateTime|null $lifetime
55
     * @param String $uploadConfiguration
56
     * @return \Alpixel\Bundle\MediaBundle\Entity\Media
57
     */
58
    public function upload(File $file, $dest_folder = '', \DateTime $lifetime = null, $uploadConfiguration = null)
59
    {
60
        if ($file instanceof UploadedFile) {
61
            if ($file->getError() !== null && $file->getError() !== 0) {
62
                throw new UploadException($file->getErrorMessage());
63
            }
64
        }
65
66
        //preparing dir name
67
        $dest_folder = date('Ymd').'/'.date('G').'/'.$dest_folder;
68
69
        //checking mimetypes
70
        if ($uploadConfiguration !== null) {
71
            $allowedMimeTypes = $this->uploadConfiguration[$uploadConfiguration]['allowed_mimetypes'];
72
73
            $mimeTypePassed = false;
74
            foreach ($allowedMimeTypes as $mimeType) {
75
                if (preg_match('@'.$mimeType.'@', $file->getMimeType())) {
76
                    $mimeTypePassed = true;
77
                }
78
            }
79
80
            if (!$mimeTypePassed) {
81
                throw new InvalidMimeTypeException(
82
                    'Only following filetypes are allowed : '.implode(', ', $allowedMimeTypes)
83
                );
84
            }
85
        }
86
87
        $fs = new Filesystem();
88
        if (!$fs->exists($this->uploadDir.$dest_folder)) {
89
            $fs->mkdir($this->uploadDir.$dest_folder);
90
        }
91
92
        $em = $this->entityManager;
93
        $media = new Media();
94
        $media->setMime($file->getMimeType());
95
96
        // If there's one, we try to generate a new name
97
        $extension = $file->getExtension();
98
99
        // Sanitizing the filename
100
        $slugify = new Slugify();
101
        if ($file instanceof UploadedFile) {
102
            if (empty($extension)) {
103
                $extension = $file->getClientOriginalExtension();
104
                if (empty($extension)) {
105
                    $extension = $file->guessClientExtension();
106
                }
107
            }
108
            $filename = $slugify->slugify(basename($file->getClientOriginalName(), $extension)).'.'.$extension;
109
        } else {
110
            if (empty($extension)) {
111
                $extension = $file->guessClientExtension();
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 guessClientExtension() 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...
112
            }
113
            $filename = $slugify->slugify(basename($file->getFilename(), $extension)).'.'.$extension;
114
        }
115
116
        // A media can have a lifetime and will be deleted with the cleanup function
117
        if (!empty($lifetime)) {
118
            $media->setLifetime($lifetime);
119
        }
120
121
        // Checking for a media with the same name
122
        $mediaExists = $this->entityManager->getRepository('AlpixelMediaBundle:Media')->findOneByUri(
123
            $dest_folder.$filename
124
        );
125
        $mediaExists = (count($mediaExists) > 0);
126
        if ($mediaExists === false) {
127
            $mediaExists = $fs->exists($this->uploadDir.$dest_folder.$filename);
128
        }
129
130
        if ($mediaExists === true) {
131
            $filename = basename($filename, '.'.$extension);
132
            $i = 1;
133
            do {
134
                $media->setName($filename.'-'.$i++.'.'.$extension);
135
                $media->setUri($dest_folder.$media->getName());
136
                $mediaExists = $this->entityManager->getRepository('AlpixelMediaBundle:Media')->findOneByUri(
137
                    $media->getUri()
138
                );
139
                $mediaExists = (count($mediaExists) > 0);
140
                if ($mediaExists === false) {
141
                    $mediaExists = $fs->exists($this->uploadDir.$dest_folder.$filename);
142
                }
143
            } while ($mediaExists === true);
144
        } else {
145
            $media->setName($filename);
146
            $media->setUri($dest_folder.$media->getName());
147
        }
148
149
        $file->move($this->uploadDir.$dest_folder, $media->getName());
150
        chmod($this->uploadDir.$dest_folder.$media->getName(), 0664);
151
152
        // Getting the salt defined in parameters.yml
153
        $secret = $this->container->getParameter('secret');
154
        $media->setSecretKey(hash('sha256', $secret.$media->getName().$media->getUri()));
155
156
        $em->persist($media);
157
        $em->flush();
158
159
        return $media;
160
    }
161
162
    /**
163
     * @return int
164
     */
165
    public function cleanup()
166
    {
167
        $expired = 0;
168
169
        $mediaRepo = $this->entityManager->getRepository('AlpixelMediaBundle:Media');
170
171
        //Cleanup expired files
172
        $medias = $mediaRepo->findExpiredMedias();
173
        $expired += count($medias);
174
        foreach ($medias as $media) {
175
            $this->delete($media);
176
        }
177
178
        //Cleanup files without any database record
179
        $fs = new Filesystem();
180
        $finder = new Finder();
181
        $files = $finder->in($this->uploadDir)->exclude('filters')->files();
182
183
        foreach ($files as $file) {
184
            $path = $file->getRelativePathname();
185
            $media = $mediaRepo->findOneByUri($path);
186
            if ($media === null) {
187
                try {
188
                    $fs->remove($file);
189
                    $expired++;
190
                } catch (IOException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
191
                }
192
            }
193
        }
194
195
        return $expired;
196
    }
197
198
    public function delete(Media $media, $flush = true)
199
    {
200
        $em = $this->entityManager;
201
        $fs = new Filesystem();
202
203
        $file_path = $this->uploadDir.$media->getUri();
204
205
        try {
206
            $file = new File($file_path);
207
            if ($file->isFile() && $file->isWritable()) {
208
                $fs->remove($file_path);
209
            }
210
        } catch (FileNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
211
        }
212
213
        $em->remove($media);
214
        if ($flush) {
215
            $em->flush();
216
        }
217
    }
218
219
    public function getUploadDir($filter = null)
220
    {
221
        if (!empty($filter)) {
222
            return $this->uploadDir.'filters/'.$filter.'/';
223
        }
224
225
        return $this->uploadDir;
226
    }
227
228
    public function getWebPath(Media $media)
229
    {
230
        $request = $this->container->get('request');
231
        $dir = $request->getSchemeAndHttpHost().$request->getBaseUrl().'/';
232
233
        return $dir.$media->getUri();
234
    }
235
236
    public function getAbsolutePath(Media $media, $filter = null)
237
    {
238
        $imgSrc = $this->uploadDir;
239
        if (!empty($filter)) {
240
            return $imgSrc.'filters/'.$filter.'/'.$media->getUri();
241
        } else {
242
            return $imgSrc.$media->getUri();
243
        }
244
    }
245
246
    public function generateUrl(Media $media, $options)
247
    {
248
        $defaultOptions = [
249
            'public'   => true,
250
            'action'   => 'show',
251
            'filter'   => null,
252
            'absolute' => false,
253
        ];
254
255
        $options = array_merge($defaultOptions, $options);
256
        $params = [];
257
258
        $routeName = 'media_';
259
        if ($options['action'] === 'download') {
260
            $routeName .= 'download_';
261
        } else {
262
            $routeName .= 'show_';
263
        }
264
265
        if ($options['public'] === true) {
266
            $routeName .= 'public';
267
            $params['id'] = $media->getId();
268
            $params['name'] = $media->getName();
269
        } else {
270
            $routeName .= 'private';
271
            $params['secretKey'] = $media->getSecretKey();
272
        }
273
274
        if ($options['filter'] !== null) {
275
            if ($options['public'] === true) {
276
                $routeName .= '_filters';
277
            }
278
            $params['filter'] = $options['filter'];
279
        }
280
281
        $container = $this->container;
282
        $router = $container->get('router');
283
284
        if ($options['absolute']) {
285
            $referenceType = UrlGenerator::ABSOLUTE_URL;
286
        } else {
287
            $referenceType = UrlGenerator::ABSOLUTE_PATH;
288
        }
289
290
        return $router->generate($routeName, $params, $referenceType);
291
    }
292
293
    public function findFromSecret($secret)
294
    {
295
        return $this->entityManager->getRepository('AlpixelMediaBundle:Media')->findOneBySecretKey($secret);
296
    }
297
298
    public function convertOctetIn($size, $convert)
299
    {
300
        if ($convert > 0) {
301
            $size = ($size / self::SIZE_OF_KIBIOCTET) * 1;
302
303
            return $this->convertOctetIn($size, $convert - 1);
304
        }
305
306
        return $size;
307
    }
308
}
309