Completed
Push — master ( 4b87b5...55d77f )
by Sullivan
03:57
created

FileProvider::setFileContents()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
nc 8
nop 2
dl 0
loc 19
rs 8.8571
c 3
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\File;
27
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
28
use Symfony\Component\HttpFoundation\File\UploadedFile;
29
use Symfony\Component\HttpFoundation\Request;
30
use Symfony\Component\HttpFoundation\StreamedResponse;
31
use Symfony\Component\Validator\Constraints\NotBlank;
32
use Symfony\Component\Validator\Constraints\NotNull;
33
34
class FileProvider extends BaseProvider
35
{
36
    protected $allowedExtensions;
37
38
    protected $allowedMimeTypes;
39
40
    protected $metadata;
41
42
    /**
43
     * @param string                   $name
44
     * @param Filesystem               $filesystem
45
     * @param CDNInterface             $cdn
46
     * @param GeneratorInterface       $pathGenerator
47
     * @param ThumbnailInterface       $thumbnail
48
     * @param array                    $allowedExtensions
49
     * @param array                    $allowedMimeTypes
50
     * @param MetadataBuilderInterface $metadata
51
     */
52
    public function __construct($name, Filesystem $filesystem, CDNInterface $cdn, GeneratorInterface $pathGenerator, ThumbnailInterface $thumbnail, array $allowedExtensions = array(), array $allowedMimeTypes = array(), MetadataBuilderInterface $metadata = null)
53
    {
54
        parent::__construct($name, $filesystem, $cdn, $pathGenerator, $thumbnail);
55
56
        $this->allowedExtensions = $allowedExtensions;
57
        $this->allowedMimeTypes  = $allowedMimeTypes;
58
        $this->metadata = $metadata;
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function getProviderMetadata()
65
    {
66
        return new Metadata($this->getName(), $this->getName().'.description', false, 'SonataMediaBundle', array('class' => 'fa fa-file-text-o'));
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    public function getReferenceImage(MediaInterface $media)
73
    {
74
        return sprintf('%s/%s',
75
            $this->generatePath($media),
76
            $media->getProviderReference()
77
        );
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function getReferenceFile(MediaInterface $media)
84
    {
85
        return $this->getFilesystem()->get($this->getReferenceImage($media), true);
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function buildEditForm(FormMapper $formMapper)
92
    {
93
        $formMapper->add('name');
94
        $formMapper->add('enabled', null, array('required' => false));
95
        $formMapper->add('authorName');
96
        $formMapper->add('cdnIsFlushable');
97
        $formMapper->add('description');
98
        $formMapper->add('copyright');
99
        $formMapper->add('binaryContent', 'file', array('required' => false));
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function buildCreateForm(FormMapper $formMapper)
106
    {
107
        $formMapper->add('binaryContent', 'file', array(
108
            'constraints' => array(
109
                new NotBlank(),
110
                new NotNull(),
111
            ),
112
        ));
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function buildMediaType(FormBuilder $formBuilder)
119
    {
120
        if ($formBuilder->getOption('context') == 'api') {
121
            $formBuilder->add('binaryContent', 'file');
122
            $formBuilder->add('contentType');
123
        } else {
124
            $formBuilder->add('binaryContent', 'file', array(
125
                'required' => false,
126
                'label'    => 'widget_label_binary_content',
127
            ));
128
        }
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function postPersist(MediaInterface $media)
135
    {
136
        if ($media->getBinaryContent() === null) {
137
            return;
138
        }
139
140
        $this->setFileContents($media);
141
142
        $this->generateThumbnails($media);
143
144
        $media->resetBinaryContent();
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public function postUpdate(MediaInterface $media)
151
    {
152
        if (!$media->getBinaryContent() instanceof \SplFileInfo) {
153
            return;
154
        }
155
156
        // Delete the current file from the FS
157
        $oldMedia = clone $media;
158
        // if no previous reference is provided, it prevents
159
        // Filesystem from trying to remove a directory
160
        if ($media->getPreviousProviderReference() !== null) {
161
            $oldMedia->setProviderReference($media->getPreviousProviderReference());
162
163
            $path = $this->getReferenceImage($oldMedia);
164
165
            if ($this->getFilesystem()->has($path)) {
166
                $this->getFilesystem()->delete($path);
167
            }
168
        }
169
170
        $this->fixBinaryContent($media);
171
172
        $this->setFileContents($media);
173
174
        $this->generateThumbnails($media);
175
176
        $media->resetBinaryContent();
177
    }
178
179
    /**
180
     * @param MediaInterface $media
181
     */
182
    protected function fixBinaryContent(MediaInterface $media)
183
    {
184
        if ($media->getBinaryContent() === null || $media->getBinaryContent() instanceof File) {
185
            return;
186
        }
187
188
        if ($media->getBinaryContent() instanceof Request) {
189
            $this->generateBinaryFromRequest($media);
190
            $this->updateMetadata($media);
191
192
            return;
193
        }
194
195
        // if the binary content is a filename => convert to a valid File
196
        if (!is_file($media->getBinaryContent())) {
197
            throw new \RuntimeException('The file does not exist : '.$media->getBinaryContent());
198
        }
199
200
        $binaryContent = new File($media->getBinaryContent());
201
        $media->setBinaryContent($binaryContent);
202
    }
203
204
    /**
205
     * @throws \RuntimeException
206
     *
207
     * @param MediaInterface $media
208
     */
209
    protected function fixFilename(MediaInterface $media)
210
    {
211
        if ($media->getBinaryContent() instanceof UploadedFile) {
212
            $media->setName($media->getName() ?: $media->getBinaryContent()->getClientOriginalName());
213
            $media->setMetadataValue('filename', $media->getBinaryContent()->getClientOriginalName());
214
        } elseif ($media->getBinaryContent() instanceof File) {
215
            $media->setName($media->getName() ?: $media->getBinaryContent()->getBasename());
216
            $media->setMetadataValue('filename', $media->getBinaryContent()->getBasename());
217
        }
218
219
        // this is the original name
220
        if (!$media->getName()) {
221
            throw new \RuntimeException('Please define a valid media\'s name');
222
        }
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228
    protected function doTransform(MediaInterface $media)
229
    {
230
        $this->fixBinaryContent($media);
231
        $this->fixFilename($media);
232
233
        // this is the name used to store the file
234
        if (!$media->getProviderReference() ||
235
            $media->getProviderReference() === MediaInterface::MISSING_BINARY_REFERENCE
236
        ) {
237
            $media->setProviderReference($this->generateReferenceName($media));
238
        }
239
240
        if ($media->getBinaryContent() instanceof File) {
241
            $media->setContentType($media->getBinaryContent()->getMimeType());
242
            $media->setSize($media->getBinaryContent()->getSize());
243
        }
244
245
        $media->setProviderStatus(MediaInterface::STATUS_OK);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function updateMetadata(MediaInterface $media, $force = true)
252
    {
253
        if (!$media->getBinaryContent() instanceof \SplFileInfo) {
254
            // this is now optimized at all!!!
255
            $path       = tempnam(sys_get_temp_dir(), 'sonata_update_metadata_');
256
            $fileObject = new \SplFileObject($path, 'w');
257
            $fileObject->fwrite($this->getReferenceFile($media)->getContent());
258
        } else {
259
            $fileObject = $media->getBinaryContent();
260
        }
261
262
        $media->setSize($fileObject->getSize());
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function generatePublicUrl(MediaInterface $media, $format)
269
    {
270
        if ($format == 'reference') {
271
            $path = $this->getReferenceImage($media);
272
        } else {
273
            // @todo: fix the asset path
274
            $path = sprintf('sonatamedia/files/%s/file.png', $format);
275
        }
276
277
        return $this->getCdn()->getPath($path, $media->getCdnIsFlushable());
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function getHelperProperties(MediaInterface $media, $format, $options = array())
284
    {
285
        return array_merge(array(
286
            'title'       => $media->getName(),
287
            'thumbnail'   => $this->getReferenceImage($media),
288
            'file'        => $this->getReferenceImage($media),
289
        ), $options);
290
    }
291
292
    /**
293
     * {@inheritdoc}
294
     */
295
    public function generatePrivateUrl(MediaInterface $media, $format)
296
    {
297
        if ($format == 'reference') {
298
            return $this->getReferenceImage($media);
299
        }
300
301
        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...
302
    }
303
304
    /**
305
     * Set the file contents for an image.
306
     *
307
     * @param MediaInterface $media
308
     * @param string         $contents path to contents, defaults to MediaInterface BinaryContent
309
     */
310
    protected function setFileContents(MediaInterface $media, $contents = null)
311
    {
312
        $file = $this->getFilesystem()->get(sprintf('%s/%s', $this->generatePath($media), $media->getProviderReference()), true);
313
        $metadata = $this->metadata ? $this->metadata->get($media, $file->getName()) : array();
314
315
        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...
316
            $file->setContent($contents, $metadata);
317
318
            return;
319
        }
320
321
        $binaryContent = $media->getBinaryContent();
322
        if ($binaryContent instanceof File) {
323
            $path = $binaryContent->getRealPath() ?: $binaryContent->getPathname();
324
            $file->setContent(file_get_contents($path), $metadata);
325
326
            return;
327
        }
328
    }
329
330
    /**
331
     * @param MediaInterface $media
332
     *
333
     * @return string
334
     */
335
    protected function generateReferenceName(MediaInterface $media)
336
    {
337
        return $this->generateMediaUniqId($media).'.'.$media->getBinaryContent()->guessExtension();
338
    }
339
340
    /**
341
     * @param MediaInterface $media
342
     *
343
     * @return string
344
     */
345
    protected function generateMediaUniqId(MediaInterface $media)
346
    {
347
        return sha1($media->getName().uniqid().rand(11111, 99999));
348
    }
349
350
    /**
351
     * {@inheritdoc}
352
     */
353
    public function getDownloadResponse(MediaInterface $media, $format, $mode, array $headers = array())
354
    {
355
        // build the default headers
356
        $headers = array_merge(array(
357
            'Content-Type'          => $media->getContentType(),
358
            'Content-Disposition'   => sprintf('attachment; filename="%s"', $media->getMetadataValue('filename')),
359
        ), $headers);
360
361
        if (!in_array($mode, array('http', 'X-Sendfile', 'X-Accel-Redirect'))) {
362
            throw new \RuntimeException('Invalid mode provided');
363
        }
364
365
        if ($mode == 'http') {
366
            if ($format == 'reference') {
367
                $file = $this->getReferenceFile($media);
368
            } else {
369
                $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...
370
            }
371
372
            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...
373
                echo $file->getContent();
374
            }, 200, $headers);
375
        }
376
377
        if (!$this->getFilesystem()->getAdapter() instanceof \Sonata\MediaBundle\Filesystem\Local) {
378
            throw new \RuntimeException('Cannot use X-Sendfile or X-Accel-Redirect with non \Sonata\MediaBundle\Filesystem\Local');
379
        }
380
381
        $filename = sprintf('%s/%s',
382
            $this->getFilesystem()->getAdapter()->getDirectory(),
383
            $this->generatePrivateUrl($media, $format)
384
        );
385
386
        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...
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     */
392
    public function validate(ErrorElement $errorElement, MediaInterface $media)
393
    {
394
        if (!$media->getBinaryContent() instanceof \SplFileInfo) {
395
            return;
396
        }
397
398
        if ($media->getBinaryContent() instanceof UploadedFile) {
399
            $fileName = $media->getBinaryContent()->getClientOriginalName();
400
        } elseif ($media->getBinaryContent() instanceof File) {
401
            $fileName = $media->getBinaryContent()->getFilename();
402
        } else {
403
            throw new \RuntimeException(sprintf('Invalid binary content type: %s', get_class($media->getBinaryContent())));
404
        }
405
406
        if (!in_array(strtolower(pathinfo($fileName, PATHINFO_EXTENSION)), $this->allowedExtensions)) {
407
            $errorElement
408
                ->with('binaryContent')
409
                ->addViolation('Invalid extensions')
410
                ->end();
411
        }
412
413
        if (!in_array($media->getBinaryContent()->getMimeType(), $this->allowedMimeTypes)) {
414
            $errorElement
415
                ->with('binaryContent')
416
                    ->addViolation('Invalid mime type : %type%', array('%type%' => $media->getBinaryContent()->getMimeType()))
417
                ->end();
418
        }
419
    }
420
421
    /**
422
     * Set media binary content according to request content.
423
     *
424
     * @param MediaInterface $media
425
     */
426
    protected function generateBinaryFromRequest(MediaInterface $media)
427
    {
428
        if (php_sapi_name() === 'cli') {
429
            throw new \RuntimeException('The current process cannot be executed in cli environment');
430
        }
431
432
        if (!$media->getContentType()) {
433
            throw new \RuntimeException(
434
                'You must provide the content type value for your media before setting the binary content'
435
            );
436
        }
437
438
        $request = $media->getBinaryContent();
439
440
        if (!$request instanceof Request) {
441
            throw new \RuntimeException('Expected Request in binary content');
442
        }
443
444
        $content = $request->getContent();
445
446
        // create unique id for media reference
447
        $guesser = ExtensionGuesser::getInstance();
448
        $extension = $guesser->guess($media->getContentType());
449
450
        if (!$extension) {
451
            throw new \RuntimeException(
452
                sprintf('Unable to guess extension for content type %s', $media->getContentType())
453
            );
454
        }
455
456
        $handle = tmpfile();
457
        fwrite($handle, $content);
458
        $file = new ApiMediaFile($handle);
459
        $file->setExtension($extension);
460
        $file->setMimetype($media->getContentType());
461
462
        $media->setBinaryContent($file);
463
    }
464
}
465