1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of the Sonata Project package. |
7
|
|
|
* |
8
|
|
|
* (c) Thomas Rabaix <[email protected]> |
9
|
|
|
* |
10
|
|
|
* For the full copyright and license information, please view the LICENSE |
11
|
|
|
* file that was distributed with this source code. |
12
|
|
|
*/ |
13
|
|
|
|
14
|
|
|
namespace Sonata\MediaBundle\Provider; |
15
|
|
|
|
16
|
|
|
use Gaufrette\Filesystem; |
17
|
|
|
use Sonata\AdminBundle\Form\FormMapper; |
18
|
|
|
use Sonata\Form\Validator\ErrorElement; |
19
|
|
|
use Sonata\MediaBundle\CDN\CDNInterface; |
20
|
|
|
use Sonata\MediaBundle\Extra\ApiMediaFile; |
21
|
|
|
use Sonata\MediaBundle\Filesystem\Local; |
22
|
|
|
use Sonata\MediaBundle\Generator\GeneratorInterface; |
23
|
|
|
use Sonata\MediaBundle\Metadata\MetadataBuilderInterface; |
24
|
|
|
use Sonata\MediaBundle\Model\MediaInterface; |
25
|
|
|
use Sonata\MediaBundle\Thumbnail\ThumbnailInterface; |
26
|
|
|
use Symfony\Component\Form\Extension\Core\Type\FileType; |
27
|
|
|
use Symfony\Component\Form\FormBuilder; |
28
|
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse; |
29
|
|
|
use Symfony\Component\HttpFoundation\File\Exception\UploadException; |
30
|
|
|
use Symfony\Component\HttpFoundation\File\File; |
31
|
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile; |
32
|
|
|
use Symfony\Component\HttpFoundation\Request; |
33
|
|
|
use Symfony\Component\HttpFoundation\StreamedResponse; |
34
|
|
|
use Symfony\Component\Mime\MimeTypes; |
35
|
|
|
use Symfony\Component\Validator\Constraints\NotBlank; |
36
|
|
|
use Symfony\Component\Validator\Constraints\NotNull; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @final since sonata-project/media-bundle 3.21.0 |
40
|
|
|
*/ |
41
|
|
|
class FileProvider extends BaseProvider implements FileProviderInterface |
42
|
|
|
{ |
43
|
|
|
protected $allowedExtensions; |
44
|
|
|
|
45
|
|
|
protected $allowedMimeTypes; |
46
|
|
|
|
47
|
|
|
protected $metadata; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @param string $name |
51
|
|
|
* @param MetadataBuilderInterface $metadata |
52
|
|
|
*/ |
53
|
|
|
public function __construct($name, Filesystem $filesystem, CDNInterface $cdn, GeneratorInterface $pathGenerator, ThumbnailInterface $thumbnail, array $allowedExtensions = [], array $allowedMimeTypes = [], ?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
|
|
|
public function getProviderMetadata() |
63
|
|
|
{ |
64
|
|
|
return new Metadata( |
65
|
|
|
$this->getName(), |
66
|
|
|
$this->getName().'.description', |
67
|
|
|
null, |
68
|
|
|
'SonataMediaBundle', |
69
|
|
|
['class' => 'fa fa-file-text-o'] |
70
|
|
|
); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
public function getReferenceImage(MediaInterface $media) |
74
|
|
|
{ |
75
|
|
|
return sprintf( |
76
|
|
|
'%s/%s', |
77
|
|
|
$this->generatePath($media), |
78
|
|
|
$media->getProviderReference() |
79
|
|
|
); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
public function getReferenceFile(MediaInterface $media) |
83
|
|
|
{ |
84
|
|
|
return $this->getFilesystem()->get($this->getReferenceImage($media), true); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* @return string[] |
89
|
|
|
*/ |
90
|
|
|
public function getAllowedExtensions() |
91
|
|
|
{ |
92
|
|
|
return $this->allowedExtensions; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @return string[] |
97
|
|
|
*/ |
98
|
|
|
public function getAllowedMimeTypes() |
99
|
|
|
{ |
100
|
|
|
return $this->allowedMimeTypes; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
public function buildEditForm(FormMapper $formMapper): void |
104
|
|
|
{ |
105
|
|
|
$formMapper->add('name'); |
106
|
|
|
$formMapper->add('enabled', null, ['required' => false]); |
107
|
|
|
$formMapper->add('authorName'); |
108
|
|
|
$formMapper->add('cdnIsFlushable'); |
109
|
|
|
$formMapper->add('description'); |
110
|
|
|
$formMapper->add('copyright'); |
111
|
|
|
$formMapper->add('binaryContent', FileType::class, ['required' => false]); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
public function buildCreateForm(FormMapper $formMapper): void |
115
|
|
|
{ |
116
|
|
|
$formMapper->add('binaryContent', FileType::class, [ |
117
|
|
|
'constraints' => [ |
118
|
|
|
new NotBlank(), |
119
|
|
|
new NotNull(), |
120
|
|
|
], |
121
|
|
|
]); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
public function buildMediaType(FormBuilder $formBuilder): void |
125
|
|
|
{ |
126
|
|
|
if ('api' === $formBuilder->getOption('context')) { |
127
|
|
|
$formBuilder->add('binaryContent', FileType::class); |
128
|
|
|
$formBuilder->add('contentType'); |
129
|
|
|
} else { |
130
|
|
|
$formBuilder->add('binaryContent', FileType::class, [ |
131
|
|
|
'required' => false, |
132
|
|
|
'label' => 'widget_label_binary_content', |
133
|
|
|
]); |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
public function postPersist(MediaInterface $media): void |
138
|
|
|
{ |
139
|
|
|
if (null === $media->getBinaryContent()) { |
140
|
|
|
return; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
$this->setFileContents($media); |
144
|
|
|
|
145
|
|
|
$this->generateThumbnails($media); |
146
|
|
|
|
147
|
|
|
$media->resetBinaryContent(); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
public function postUpdate(MediaInterface $media): void |
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 (null !== $media->getPreviousProviderReference()) { |
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
|
|
|
public function updateMetadata(MediaInterface $media, $force = true): void |
180
|
|
|
{ |
181
|
|
|
if (!$media->getBinaryContent() instanceof \SplFileInfo) { |
182
|
|
|
// this is now optimized at all!!! |
183
|
|
|
$path = tempnam(sys_get_temp_dir(), 'sonata_update_metadata_'); |
184
|
|
|
$fileObject = new \SplFileObject($path, 'w'); |
185
|
|
|
$fileObject->fwrite($this->getReferenceFile($media)->getContent()); |
186
|
|
|
} else { |
187
|
|
|
$fileObject = $media->getBinaryContent(); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
$media->setSize($fileObject->getSize()); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
public function generatePublicUrl(MediaInterface $media, $format) |
194
|
|
|
{ |
195
|
|
|
if (MediaProviderInterface::FORMAT_REFERENCE === $format) { |
196
|
|
|
$path = $this->getReferenceImage($media); |
197
|
|
|
} else { |
198
|
|
|
// @todo: fix the asset path |
199
|
|
|
$path = sprintf('sonatamedia/files/%s/file.png', $format); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
return $this->getCdn()->getPath($path, $media->getCdnIsFlushable()); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
public function getHelperProperties(MediaInterface $media, $format, $options = []) |
206
|
|
|
{ |
207
|
|
|
return array_merge([ |
208
|
|
|
'title' => $media->getName(), |
209
|
|
|
'thumbnail' => $this->getReferenceImage($media), |
210
|
|
|
'file' => $this->getReferenceImage($media), |
211
|
|
|
], $options); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
public function generatePrivateUrl(MediaInterface $media, $format) |
215
|
|
|
{ |
216
|
|
|
if (MediaProviderInterface::FORMAT_REFERENCE === $format) { |
217
|
|
|
return $this->getReferenceImage($media); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return false; |
|
|
|
|
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
public function getDownloadResponse(MediaInterface $media, $format, $mode, array $headers = []) |
224
|
|
|
{ |
225
|
|
|
// build the default headers |
226
|
|
|
$headers = array_merge([ |
227
|
|
|
'Content-Type' => $media->getContentType(), |
228
|
|
|
'Content-Disposition' => sprintf('attachment; filename="%s"', $media->getMetadataValue('filename')), |
229
|
|
|
], $headers); |
230
|
|
|
|
231
|
|
|
if (!\in_array($mode, ['http', 'X-Sendfile', 'X-Accel-Redirect'], true)) { |
232
|
|
|
throw new \RuntimeException('Invalid mode provided'); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if ('http' === $mode) { |
236
|
|
|
if (MediaProviderInterface::FORMAT_REFERENCE === $format) { |
237
|
|
|
$file = $this->getReferenceFile($media); |
238
|
|
|
} else { |
239
|
|
|
$file = $this->getFilesystem()->get($this->generatePrivateUrl($media, $format)); |
|
|
|
|
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
return new StreamedResponse(static function () use ($file): void { |
243
|
|
|
echo $file->getContent(); |
244
|
|
|
}, 200, $headers); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
if (!$this->getFilesystem()->getAdapter() instanceof Local) { |
248
|
|
|
throw new \RuntimeException('Cannot use X-Sendfile or X-Accel-Redirect with non \Sonata\MediaBundle\Filesystem\Local'); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
$filename = sprintf( |
252
|
|
|
'%s/%s', |
253
|
|
|
$this->getFilesystem()->getAdapter()->getDirectory(), |
254
|
|
|
$this->generatePrivateUrl($media, $format) |
255
|
|
|
); |
256
|
|
|
|
257
|
|
|
return new BinaryFileResponse($filename, 200, $headers); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
public function validate(ErrorElement $errorElement, MediaInterface $media): void |
261
|
|
|
{ |
262
|
|
|
if (!$media->getBinaryContent() instanceof \SplFileInfo) { |
263
|
|
|
return; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
if ($media->getBinaryContent() instanceof UploadedFile) { |
267
|
|
|
$fileName = $media->getBinaryContent()->getClientOriginalName(); |
268
|
|
|
} elseif ($media->getBinaryContent() instanceof File) { |
269
|
|
|
$fileName = $media->getBinaryContent()->getFilename(); |
270
|
|
|
} else { |
271
|
|
|
throw new \RuntimeException(sprintf('Invalid binary content type: %s', \get_class($media->getBinaryContent()))); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
if ($media->getBinaryContent() instanceof UploadedFile && 0 === ($media->getBinaryContent()->getSize() ?: 0)) { |
275
|
|
|
$errorElement |
276
|
|
|
->with('binaryContent') |
277
|
|
|
->addViolation( |
278
|
|
|
'The file is too big, max size: %maxFileSize%', |
279
|
|
|
['%maxFileSize%' => ini_get('upload_max_filesize')] |
280
|
|
|
) |
281
|
|
|
->end(); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
if (!\in_array(strtolower(pathinfo($fileName, PATHINFO_EXTENSION)), $this->allowedExtensions, true)) { |
285
|
|
|
$errorElement |
286
|
|
|
->with('binaryContent') |
287
|
|
|
->addViolation('Invalid extensions') |
288
|
|
|
->end(); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
if ('' !== $media->getBinaryContent()->getFilename() && !\in_array($media->getBinaryContent()->getMimeType(), $this->allowedMimeTypes, true)) { |
292
|
|
|
$errorElement |
293
|
|
|
->with('binaryContent') |
294
|
|
|
->addViolation('Invalid mime type : %type%', ['%type%' => $media->getBinaryContent()->getMimeType()]) |
295
|
|
|
->end(); |
296
|
|
|
} |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
protected function fixBinaryContent(MediaInterface $media): void |
300
|
|
|
{ |
301
|
|
|
if (null === $media->getBinaryContent() || $media->getBinaryContent() instanceof File) { |
302
|
|
|
return; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
if ($media->getBinaryContent() instanceof Request) { |
306
|
|
|
$this->generateBinaryFromRequest($media); |
307
|
|
|
$this->updateMetadata($media); |
308
|
|
|
|
309
|
|
|
return; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
// if the binary content is a filename => convert to a valid File |
313
|
|
|
if (!is_file($media->getBinaryContent())) { |
314
|
|
|
throw new \RuntimeException('The file does not exist : '.$media->getBinaryContent()); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
$binaryContent = new File($media->getBinaryContent()); |
318
|
|
|
$media->setBinaryContent($binaryContent); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* @throws \RuntimeException |
323
|
|
|
*/ |
324
|
|
|
protected function fixFilename(MediaInterface $media): void |
325
|
|
|
{ |
326
|
|
|
if ($media->getBinaryContent() instanceof UploadedFile) { |
327
|
|
|
$media->setName($media->getName() ?: $media->getBinaryContent()->getClientOriginalName()); |
328
|
|
|
$media->setMetadataValue('filename', $media->getBinaryContent()->getClientOriginalName()); |
329
|
|
|
} elseif ($media->getBinaryContent() instanceof File) { |
330
|
|
|
$media->setName($media->getName() ?: $media->getBinaryContent()->getBasename()); |
331
|
|
|
$media->setMetadataValue('filename', $media->getBinaryContent()->getBasename()); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
// this is the original name |
335
|
|
|
if (!$media->getName()) { |
336
|
|
|
throw new \RuntimeException('Please define a valid media\'s name'); |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
protected function doTransform(MediaInterface $media): void |
341
|
|
|
{ |
342
|
|
|
$this->fixBinaryContent($media); |
343
|
|
|
$this->fixFilename($media); |
344
|
|
|
|
345
|
|
|
if ($media->getBinaryContent() instanceof UploadedFile && 0 === $media->getBinaryContent()->getSize()) { |
346
|
|
|
$media->setProviderReference(uniqid($media->getName(), true)); |
347
|
|
|
$media->setProviderStatus(MediaInterface::STATUS_ERROR); |
348
|
|
|
|
349
|
|
|
throw new UploadException('The uploaded file is not found'); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
// this is the name used to store the file |
353
|
|
|
if (!$media->getProviderReference() || |
354
|
|
|
MediaInterface::MISSING_BINARY_REFERENCE === $media->getProviderReference() |
355
|
|
|
) { |
356
|
|
|
$media->setProviderReference($this->generateReferenceName($media)); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
if ($media->getBinaryContent() instanceof File) { |
360
|
|
|
$media->setContentType($media->getBinaryContent()->getMimeType()); |
361
|
|
|
$media->setSize($media->getBinaryContent()->getSize()); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
$media->setProviderStatus(MediaInterface::STATUS_OK); |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Set the file contents for an image. |
369
|
|
|
* |
370
|
|
|
* @param string $contents path to contents, defaults to MediaInterface BinaryContent |
371
|
|
|
*/ |
372
|
|
|
protected function setFileContents(MediaInterface $media, $contents = null): void |
373
|
|
|
{ |
374
|
|
|
$file = $this->getFilesystem()->get(sprintf('%s/%s', $this->generatePath($media), $media->getProviderReference()), true); |
375
|
|
|
$metadata = $this->metadata ? $this->metadata->get($media, $file->getName()) : []; |
376
|
|
|
|
377
|
|
|
if ($contents) { |
|
|
|
|
378
|
|
|
$file->setContent($contents, $metadata); |
379
|
|
|
|
380
|
|
|
return; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
$binaryContent = $media->getBinaryContent(); |
384
|
|
|
if ($binaryContent instanceof File) { |
385
|
|
|
$path = $binaryContent->getRealPath() ?: $binaryContent->getPathname(); |
386
|
|
|
$file->setContent(file_get_contents($path), $metadata); |
387
|
|
|
|
388
|
|
|
return; |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* @return string |
394
|
|
|
*/ |
395
|
|
|
protected function generateReferenceName(MediaInterface $media) |
396
|
|
|
{ |
397
|
|
|
return $this->generateMediaUniqId($media).'.'.$media->getBinaryContent()->guessExtension(); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* @return string |
402
|
|
|
*/ |
403
|
|
|
protected function generateMediaUniqId(MediaInterface $media) |
404
|
|
|
{ |
405
|
|
|
return sha1($media->getName().uniqid().random_int(11111, 99999)); |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Set media binary content according to request content. |
410
|
|
|
*/ |
411
|
|
|
protected function generateBinaryFromRequest(MediaInterface $media): void |
412
|
|
|
{ |
413
|
|
|
if (!$media->getContentType()) { |
414
|
|
|
throw new \RuntimeException( |
415
|
|
|
'You must provide the content type value for your media before setting the binary content' |
416
|
|
|
); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
$request = $media->getBinaryContent(); |
420
|
|
|
|
421
|
|
|
if (!$request instanceof Request) { |
422
|
|
|
throw new \RuntimeException('Expected Request in binary content'); |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
$content = $request->getContent(); |
426
|
|
|
|
427
|
|
|
// create unique id for media reference |
428
|
|
|
$guesser = MimeTypes::getDefault(); |
429
|
|
|
$extensions = $guesser->getExtensions($media->getContentType()); |
430
|
|
|
$extension = $extensions[0] ?? null; |
431
|
|
|
|
432
|
|
|
if (!$extension) { |
433
|
|
|
throw new \RuntimeException( |
434
|
|
|
sprintf('Unable to guess extension for content type %s', $media->getContentType()) |
435
|
|
|
); |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
$handle = tmpfile(); |
439
|
|
|
fwrite($handle, $content); |
440
|
|
|
$file = new ApiMediaFile($handle); |
441
|
|
|
$file->setExtension($extension); |
442
|
|
|
$file->setMimetype($media->getContentType()); |
443
|
|
|
|
444
|
|
|
$media->setBinaryContent($file); |
445
|
|
|
} |
446
|
|
|
} |
447
|
|
|
|
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.