Completed
Pull Request — master (#1175)
by Grégoire
03:04
created

FileProvider::doTransform()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 5
nop 1
dl 0
loc 25
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Sonata Project package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\MediaBundle\Provider;
13
14
use Gaufrette\Filesystem;
15
use Sonata\AdminBundle\Form\FormMapper;
16
use Sonata\CoreBundle\Model\Metadata;
17
use Sonata\CoreBundle\Validator\ErrorElement;
18
use Sonata\MediaBundle\CDN\CDNInterface;
19
use Sonata\MediaBundle\Extra\ApiMediaFile;
20
use Sonata\MediaBundle\Generator\GeneratorInterface;
21
use Sonata\MediaBundle\Metadata\MetadataBuilderInterface;
22
use Sonata\MediaBundle\Model\MediaInterface;
23
use Sonata\MediaBundle\Thumbnail\ThumbnailInterface;
24
use Symfony\Component\Form\FormBuilder;
25
use Symfony\Component\HttpFoundation\BinaryFileResponse;
26
use Symfony\Component\HttpFoundation\File\Exception\UploadException;
27
use Symfony\Component\HttpFoundation\File\File;
28
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
29
use Symfony\Component\HttpFoundation\File\UploadedFile;
30
use Symfony\Component\HttpFoundation\Request;
31
use Symfony\Component\HttpFoundation\StreamedResponse;
32
use Symfony\Component\Validator\Constraints\NotBlank;
33
use Symfony\Component\Validator\Constraints\NotNull;
34
35
class FileProvider extends BaseProvider
36
{
37
    protected $allowedExtensions;
38
39
    protected $allowedMimeTypes;
40
41
    protected $metadata;
42
43
    /**
44
     * @param string                   $name
45
     * @param Filesystem               $filesystem
46
     * @param CDNInterface             $cdn
47
     * @param GeneratorInterface       $pathGenerator
48
     * @param ThumbnailInterface       $thumbnail
49
     * @param array                    $allowedExtensions
50
     * @param array                    $allowedMimeTypes
51
     * @param MetadataBuilderInterface $metadata
52
     */
53
    public function __construct($name, Filesystem $filesystem, CDNInterface $cdn, GeneratorInterface $pathGenerator, ThumbnailInterface $thumbnail, array $allowedExtensions = array(), array $allowedMimeTypes = array(), MetadataBuilderInterface $metadata = null)
54
    {
55
        parent::__construct($name, $filesystem, $cdn, $pathGenerator, $thumbnail);
56
57
        $this->allowedExtensions = $allowedExtensions;
58
        $this->allowedMimeTypes = $allowedMimeTypes;
59
        $this->metadata = $metadata;
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function getProviderMetadata()
66
    {
67
        return new Metadata($this->getName(), $this->getName().'.description', false, 'SonataMediaBundle', array('class' => 'fa fa-file-text-o'));
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function getReferenceImage(MediaInterface $media)
74
    {
75
        return sprintf('%s/%s',
76
            $this->generatePath($media),
77
            $media->getProviderReference()
78
        );
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function getReferenceFile(MediaInterface $media)
85
    {
86
        return $this->getFilesystem()->get($this->getReferenceImage($media), true);
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public function buildEditForm(FormMapper $formMapper)
93
    {
94
        $formMapper->add('name');
95
        $formMapper->add('enabled', null, array('required' => false));
96
        $formMapper->add('authorName');
97
        $formMapper->add('cdnIsFlushable');
98
        $formMapper->add('description');
99
        $formMapper->add('copyright');
100
        $formMapper->add(
101
            'binaryContent',
102
            'Symfony\Component\Form\Extension\Core\Type\FileType',
103
            array('required' => false)
104
        );
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110
    public function buildCreateForm(FormMapper $formMapper)
111
    {
112
        $formMapper->add(
113
            'binaryContent',
114
            'Symfony\Component\Form\Extension\Core\Type\FileType',
115
            array(
116
                'constraints' => array(
117
                    new NotBlank(),
118
                    new NotNull(),
119
                ),
120
            )
121
        );
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function buildMediaType(FormBuilder $formBuilder)
128
    {
129
        $fileType = 'Symfony\Component\Form\Extension\Core\Type\FileType';
130
131
        if ($formBuilder->getOption('context') == 'api') {
132
            $formBuilder->add('binaryContent', $fileType);
133
            $formBuilder->add('contentType');
134
        } else {
135
            $formBuilder->add('binaryContent', $fileType, array(
136
                'required' => false,
137
                'label' => 'widget_label_binary_content',
138
            ));
139
        }
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function postPersist(MediaInterface $media)
146
    {
147
        if ($media->getBinaryContent() === null) {
148
            return;
149
        }
150
151
        $this->setFileContents($media);
152
153
        $this->generateThumbnails($media);
154
155
        $media->resetBinaryContent();
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function postUpdate(MediaInterface $media)
162
    {
163
        if (!$media->getBinaryContent() instanceof \SplFileInfo) {
164
            return;
165
        }
166
167
        // Delete the current file from the FS
168
        $oldMedia = clone $media;
169
        // if no previous reference is provided, it prevents
170
        // Filesystem from trying to remove a directory
171
        if ($media->getPreviousProviderReference() !== null) {
172
            $oldMedia->setProviderReference($media->getPreviousProviderReference());
173
174
            $path = $this->getReferenceImage($oldMedia);
175
176
            if ($this->getFilesystem()->has($path)) {
177
                $this->getFilesystem()->delete($path);
178
            }
179
        }
180
181
        $this->fixBinaryContent($media);
182
183
        $this->setFileContents($media);
184
185
        $this->generateThumbnails($media);
186
187
        $media->resetBinaryContent();
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193
    public function updateMetadata(MediaInterface $media, $force = true)
194
    {
195
        if (!$media->getBinaryContent() instanceof \SplFileInfo) {
196
            // this is now optimized at all!!!
197
            $path = tempnam(sys_get_temp_dir(), 'sonata_update_metadata_');
198
            $fileObject = new \SplFileObject($path, 'w');
199
            $fileObject->fwrite($this->getReferenceFile($media)->getContent());
200
        } else {
201
            $fileObject = $media->getBinaryContent();
202
        }
203
204
        $media->setSize($fileObject->getSize());
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function generatePublicUrl(MediaInterface $media, $format)
211
    {
212
        if ($format == 'reference') {
213
            $path = $this->getReferenceImage($media);
214
        } else {
215
            // @todo: fix the asset path
216
            $path = sprintf('sonatamedia/files/%s/file.png', $format);
217
        }
218
219
        return $this->getCdn()->getPath($path, $media->getCdnIsFlushable());
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function getHelperProperties(MediaInterface $media, $format, $options = array())
226
    {
227
        return array_merge(array(
228
            'title' => $media->getName(),
229
            'thumbnail' => $this->getReferenceImage($media),
230
            'file' => $this->getReferenceImage($media),
231
        ), $options);
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237
    public function generatePrivateUrl(MediaInterface $media, $format)
238
    {
239
        if ($format == 'reference') {
240
            return $this->getReferenceImage($media);
241
        }
242
243
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Sonata\MediaBundle\Provi...ace::generatePrivateUrl of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249
    public function getDownloadResponse(MediaInterface $media, $format, $mode, array $headers = array())
250
    {
251
        // build the default headers
252
        $headers = array_merge(array(
253
            'Content-Type' => $media->getContentType(),
254
            'Content-Disposition' => sprintf('attachment; filename="%s"', $media->getMetadataValue('filename')),
255
        ), $headers);
256
257
        if (!in_array($mode, array('http', 'X-Sendfile', 'X-Accel-Redirect'))) {
258
            throw new \RuntimeException('Invalid mode provided');
259
        }
260
261
        if ($mode == 'http') {
262
            if ($format == 'reference') {
263
                $file = $this->getReferenceFile($media);
264
            } else {
265
                $file = $this->getFilesystem()->get($this->generatePrivateUrl($media, $format));
0 ignored issues
show
Security Bug introduced by
It seems like $this->generatePrivateUrl($media, $format) targeting Sonata\MediaBundle\Provi...r::generatePrivateUrl() can also be of type false; however, Gaufrette\Filesystem::get() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
266
            }
267
268
            return new StreamedResponse(function () use ($file) {
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Symfony\Comp...t(); }, 200, $headers); (Symfony\Component\HttpFoundation\StreamedResponse) is incompatible with the return type declared by the interface Sonata\MediaBundle\Provi...ce::getDownloadResponse of type Sonata\MediaBundle\Provider\Response.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
269
                echo $file->getContent();
270
            }, 200, $headers);
271
        }
272
273
        if (!$this->getFilesystem()->getAdapter() instanceof \Sonata\MediaBundle\Filesystem\Local) {
274
            throw new \RuntimeException('Cannot use X-Sendfile or X-Accel-Redirect with non \Sonata\MediaBundle\Filesystem\Local');
275
        }
276
277
        $filename = sprintf('%s/%s',
278
            $this->getFilesystem()->getAdapter()->getDirectory(),
279
            $this->generatePrivateUrl($media, $format)
280
        );
281
282
        return new BinaryFileResponse($filename, 200, $headers);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Symfony\Comp...lename, 200, $headers); (Symfony\Component\HttpFo...tion\BinaryFileResponse) is incompatible with the return type declared by the interface Sonata\MediaBundle\Provi...ce::getDownloadResponse of type Sonata\MediaBundle\Provider\Response.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     */
288
    public function validate(ErrorElement $errorElement, MediaInterface $media)
289
    {
290
        if (!$media->getBinaryContent() instanceof \SplFileInfo) {
291
            return;
292
        }
293
294
        if ($media->getBinaryContent() instanceof UploadedFile) {
295
            $fileName = $media->getBinaryContent()->getClientOriginalName();
296
        } elseif ($media->getBinaryContent() instanceof File) {
297
            $fileName = $media->getBinaryContent()->getFilename();
298
        } else {
299
            throw new \RuntimeException(sprintf('Invalid binary content type: %s', get_class($media->getBinaryContent())));
300
        }
301
302
        if ($media->getBinaryContent() instanceof UploadedFile && 0 === $media->getBinaryContent()->getClientSize()) {
303
            $errorElement
304
               ->with('binaryContent')
305
                   ->addViolation('The file is too big, max size: '.ini_get('upload_max_filesize'))
306
               ->end();
307
        }
308
309
        if (!in_array(strtolower(pathinfo($fileName, PATHINFO_EXTENSION)), $this->allowedExtensions)) {
310
            $errorElement
311
                ->with('binaryContent')
312
                ->addViolation('Invalid extensions')
313
                ->end();
314
        }
315
316
        if ('' != $media->getBinaryContent()->getFilename() && !in_array($media->getBinaryContent()->getMimeType(), $this->allowedMimeTypes)) {
317
            $errorElement
318
                ->with('binaryContent')
319
                    ->addViolation('Invalid mime type : %type%', array('%type%' => $media->getBinaryContent()->getMimeType()))
320
                ->end();
321
        }
322
    }
323
324
    /**
325
     * @param MediaInterface $media
326
     */
327
    protected function fixBinaryContent(MediaInterface $media)
328
    {
329
        if ($media->getBinaryContent() === null || $media->getBinaryContent() instanceof File) {
330
            return;
331
        }
332
333
        if ($media->getBinaryContent() instanceof Request) {
334
            $this->generateBinaryFromRequest($media);
335
            $this->updateMetadata($media);
336
337
            return;
338
        }
339
340
        // if the binary content is a filename => convert to a valid File
341
        if (!is_file($media->getBinaryContent())) {
342
            throw new \RuntimeException('The file does not exist : '.$media->getBinaryContent());
343
        }
344
345
        $binaryContent = new File($media->getBinaryContent());
346
        $media->setBinaryContent($binaryContent);
347
    }
348
349
    /**
350
     * @throws \RuntimeException
351
     *
352
     * @param MediaInterface $media
353
     */
354
    protected function fixFilename(MediaInterface $media)
355
    {
356
        if ($media->getBinaryContent() instanceof UploadedFile) {
357
            $media->setName($media->getName() ?: $media->getBinaryContent()->getClientOriginalName());
358
            $media->setMetadataValue('filename', $media->getBinaryContent()->getClientOriginalName());
359
        } elseif ($media->getBinaryContent() instanceof File) {
360
            $media->setName($media->getName() ?: $media->getBinaryContent()->getBasename());
361
            $media->setMetadataValue('filename', $media->getBinaryContent()->getBasename());
362
        }
363
364
        // this is the original name
365
        if (!$media->getName()) {
366
            throw new \RuntimeException('Please define a valid media\'s name');
367
        }
368
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373
    protected function doTransform(MediaInterface $media)
374
    {
375
        $this->fixBinaryContent($media);
376
        $this->fixFilename($media);
377
378
        if ($media->getBinaryContent() instanceof UploadedFile && 0 === $media->getBinaryContent()->getClientSize()) {
379
            $media->setProviderReference(uniqid($media->getName(), true));
380
            $media->setProviderStatus(MediaInterface::STATUS_ERROR);
381
            throw new UploadException('The uploaded file is not found');
382
        }
383
384
        // this is the name used to store the file
385
        if (!$media->getProviderReference() ||
386
            $media->getProviderReference() === MediaInterface::MISSING_BINARY_REFERENCE
387
        ) {
388
            $media->setProviderReference($this->generateReferenceName($media));
389
        }
390
391
        if ($media->getBinaryContent() instanceof File) {
392
            $media->setContentType($media->getBinaryContent()->getMimeType());
393
            $media->setSize($media->getBinaryContent()->getSize());
394
        }
395
396
        $media->setProviderStatus(MediaInterface::STATUS_OK);
397
    }
398
399
    /**
400
     * Set the file contents for an image.
401
     *
402
     * @param MediaInterface $media
403
     * @param string         $contents path to contents, defaults to MediaInterface BinaryContent
404
     */
405
    protected function setFileContents(MediaInterface $media, $contents = null)
406
    {
407
        $file = $this->getFilesystem()->get(sprintf('%s/%s', $this->generatePath($media), $media->getProviderReference()), true);
408
        $metadata = $this->metadata ? $this->metadata->get($media, $file->getName()) : array();
409
410
        if ($contents) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $contents of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
411
            $file->setContent($contents, $metadata);
412
413
            return;
414
        }
415
416
        $binaryContent = $media->getBinaryContent();
417
        if ($binaryContent instanceof File) {
418
            $path = $binaryContent->getRealPath() ?: $binaryContent->getPathname();
419
            $file->setContent(file_get_contents($path), $metadata);
420
421
            return;
422
        }
423
    }
424
425
    /**
426
     * @param MediaInterface $media
427
     *
428
     * @return string
429
     */
430
    protected function generateReferenceName(MediaInterface $media)
431
    {
432
        return $this->generateMediaUniqId($media).'.'.$media->getBinaryContent()->guessExtension();
433
    }
434
435
    /**
436
     * @param MediaInterface $media
437
     *
438
     * @return string
439
     */
440
    protected function generateMediaUniqId(MediaInterface $media)
441
    {
442
        return sha1($media->getName().uniqid().rand(11111, 99999));
443
    }
444
445
    /**
446
     * Set media binary content according to request content.
447
     *
448
     * @param MediaInterface $media
449
     */
450
    protected function generateBinaryFromRequest(MediaInterface $media)
451
    {
452
        if (php_sapi_name() === 'cli') {
453
            throw new \RuntimeException('The current process cannot be executed in cli environment');
454
        }
455
456
        if (!$media->getContentType()) {
457
            throw new \RuntimeException(
458
                'You must provide the content type value for your media before setting the binary content'
459
            );
460
        }
461
462
        $request = $media->getBinaryContent();
463
464
        if (!$request instanceof Request) {
465
            throw new \RuntimeException('Expected Request in binary content');
466
        }
467
468
        $content = $request->getContent();
469
470
        // create unique id for media reference
471
        $guesser = ExtensionGuesser::getInstance();
472
        $extension = $guesser->guess($media->getContentType());
473
474
        if (!$extension) {
475
            throw new \RuntimeException(
476
                sprintf('Unable to guess extension for content type %s', $media->getContentType())
477
            );
478
        }
479
480
        $handle = tmpfile();
481
        fwrite($handle, $content);
482
        $file = new ApiMediaFile($handle);
483
        $file->setExtension($extension);
484
        $file->setMimetype($media->getContentType());
485
486
        $media->setBinaryContent($file);
487
    }
488
}
489