1 | <?php |
||||
2 | /** |
||||
3 | * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). |
||||
4 | * |
||||
5 | * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) |
||||
6 | * |
||||
7 | * This program is free software: you can redistribute it and/or modify |
||||
8 | * it under the terms of the GNU Affero General Public License as published |
||||
9 | * by the Free Software Foundation, either version 3 of the License, or |
||||
10 | * (at your option) any later version. |
||||
11 | * |
||||
12 | * This program is distributed in the hope that it will be useful, |
||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
15 | * GNU Affero General Public License for more details. |
||||
16 | * |
||||
17 | * You should have received a copy of the GNU Affero General Public License |
||||
18 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||
19 | */ |
||||
20 | |||||
21 | declare(strict_types=1); |
||||
22 | |||||
23 | namespace App\Services\Attachments; |
||||
24 | |||||
25 | use App\Entity\Attachments\Attachment; |
||||
26 | use App\Entity\Attachments\AttachmentContainingDBElement; |
||||
27 | use App\Entity\Attachments\AttachmentType; |
||||
28 | use App\Entity\Attachments\AttachmentTypeAttachment; |
||||
29 | use App\Entity\Attachments\CategoryAttachment; |
||||
30 | use App\Entity\Attachments\CurrencyAttachment; |
||||
31 | use App\Entity\Attachments\LabelAttachment; |
||||
32 | use App\Entity\Attachments\ProjectAttachment; |
||||
33 | use App\Entity\Attachments\FootprintAttachment; |
||||
34 | use App\Entity\Attachments\GroupAttachment; |
||||
35 | use App\Entity\Attachments\ManufacturerAttachment; |
||||
36 | use App\Entity\Attachments\MeasurementUnitAttachment; |
||||
37 | use App\Entity\Attachments\PartAttachment; |
||||
38 | use App\Entity\Attachments\StorelocationAttachment; |
||||
39 | use App\Entity\Attachments\SupplierAttachment; |
||||
40 | use App\Entity\Attachments\UserAttachment; |
||||
41 | use App\Exceptions\AttachmentDownloadException; |
||||
42 | use const DIRECTORY_SEPARATOR; |
||||
43 | use function get_class; |
||||
44 | use InvalidArgumentException; |
||||
45 | use RuntimeException; |
||||
46 | use Symfony\Component\Filesystem\Filesystem; |
||||
47 | use Symfony\Component\HttpFoundation\File\UploadedFile; |
||||
48 | use Symfony\Component\Mime\MimeTypesInterface; |
||||
49 | use Symfony\Component\OptionsResolver\OptionsResolver; |
||||
50 | use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; |
||||
51 | use Symfony\Contracts\HttpClient\HttpClientInterface; |
||||
52 | |||||
53 | /** |
||||
54 | * This service handles the form submitting of an attachment and handles things like file uploading and downloading. |
||||
55 | */ |
||||
56 | class AttachmentSubmitHandler |
||||
57 | { |
||||
58 | protected AttachmentPathResolver $pathResolver; |
||||
59 | protected array $folder_mapping; |
||||
60 | protected bool $allow_attachments_downloads; |
||||
61 | protected HttpClientInterface $httpClient; |
||||
62 | protected MimeTypesInterface $mimeTypes; |
||||
63 | protected FileTypeFilterTools $filterTools; |
||||
64 | |||||
65 | protected const BLACKLISTED_EXTENSIONS = ['php', 'phtml', 'php3', 'ph3', 'php4', 'ph4', 'php5', 'ph5', 'phtm', 'sh', |
||||
66 | 'asp', 'cgi', 'py', 'pl', 'exe', 'aspx', 'js', 'mjs', 'jsp', 'css', 'jar', 'html', 'htm', 'shtm', 'shtml', 'htaccess', |
||||
67 | 'htpasswd', '']; |
||||
68 | |||||
69 | public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads, |
||||
70 | HttpClientInterface $httpClient, MimeTypesInterface $mimeTypes, |
||||
71 | FileTypeFilterTools $filterTools) |
||||
72 | { |
||||
73 | $this->pathResolver = $pathResolver; |
||||
74 | $this->allow_attachments_downloads = $allow_attachments_downloads; |
||||
75 | $this->httpClient = $httpClient; |
||||
76 | $this->mimeTypes = $mimeTypes; |
||||
77 | |||||
78 | $this->filterTools = $filterTools; |
||||
79 | |||||
80 | //The mapping used to determine which folder will be used for an attachment type |
||||
81 | $this->folder_mapping = [ |
||||
82 | PartAttachment::class => 'part', |
||||
83 | AttachmentTypeAttachment::class => 'attachment_type', |
||||
84 | CategoryAttachment::class => 'category', |
||||
85 | CurrencyAttachment::class => 'currency', |
||||
86 | ProjectAttachment::class => 'device', |
||||
87 | FootprintAttachment::class => 'footprint', |
||||
88 | GroupAttachment::class => 'group', |
||||
89 | ManufacturerAttachment::class => 'manufacturer', |
||||
90 | MeasurementUnitAttachment::class => 'measurement_unit', |
||||
91 | StorelocationAttachment::class => 'storelocation', |
||||
92 | SupplierAttachment::class => 'supplier', |
||||
93 | UserAttachment::class => 'user', |
||||
94 | LabelAttachment::class => 'label_profile', |
||||
95 | ]; |
||||
96 | } |
||||
97 | |||||
98 | /** |
||||
99 | * Check if the extension of the uploaded file is allowed for the given attachment type. |
||||
100 | * Returns true, if the file is allowed, false if not. |
||||
101 | */ |
||||
102 | public function isValidFileExtension(AttachmentType $attachment_type, UploadedFile $uploadedFile): bool |
||||
103 | { |
||||
104 | //Only validate if the attachment type has specified an filetype filter: |
||||
105 | if (empty($attachment_type->getFiletypeFilter())) { |
||||
106 | return true; |
||||
107 | } |
||||
108 | |||||
109 | return $this->filterTools->isExtensionAllowed( |
||||
110 | $attachment_type->getFiletypeFilter(), |
||||
111 | $uploadedFile->getClientOriginalExtension() |
||||
112 | ); |
||||
113 | } |
||||
114 | |||||
115 | /** |
||||
116 | * Generates a filename for the given attachment and extension. |
||||
117 | * The filename contains a random id, so every time this function is called you get an unique name. |
||||
118 | * |
||||
119 | * @param Attachment $attachment The attachment that should be used for generating an attachment |
||||
120 | * @param string $extension The extension that the new file should have (must only contain chars allowed in pathes) |
||||
121 | * |
||||
122 | * @return string the new filename |
||||
123 | */ |
||||
124 | public function generateAttachmentFilename(Attachment $attachment, string $extension): string |
||||
125 | { |
||||
126 | //Normalize extension |
||||
127 | $extension = transliterator_transliterate( |
||||
128 | 'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', |
||||
129 | $extension |
||||
130 | ); |
||||
131 | |||||
132 | //Use the (sanatized) attachment name as an filename part |
||||
133 | $safeName = transliterator_transliterate( |
||||
134 | 'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', |
||||
135 | $attachment->getName() |
||||
136 | ); |
||||
137 | |||||
138 | return $safeName.'-'.uniqid('', false).'.'.$extension; |
||||
139 | } |
||||
140 | |||||
141 | /** |
||||
142 | * Generates an (absolute) path to a folder where the given attachment should be stored. |
||||
143 | * |
||||
144 | * @param Attachment $attachment The attachment that should be used for |
||||
145 | * @param bool $secure_upload True if the file path should be located in a safe location |
||||
146 | * |
||||
147 | * @return string the absolute path for the attachment folder |
||||
148 | */ |
||||
149 | public function generateAttachmentPath(Attachment $attachment, bool $secure_upload = false): string |
||||
150 | { |
||||
151 | if ($secure_upload) { |
||||
152 | $base_path = $this->pathResolver->getSecurePath(); |
||||
153 | } else { |
||||
154 | $base_path = $this->pathResolver->getMediaPath(); |
||||
155 | } |
||||
156 | |||||
157 | //Ensure the given attachment class is known to mapping |
||||
158 | if (!isset($this->folder_mapping[get_class($attachment)])) { |
||||
159 | throw new InvalidArgumentException('The given attachment class is not known! The passed class was: '.get_class($attachment)); |
||||
160 | } |
||||
161 | //Ensure the attachment has an assigned element |
||||
162 | if (null === $attachment->getElement()) { |
||||
163 | throw new InvalidArgumentException('The given attachment is not assigned to an element! An element is needed to generate a path!'); |
||||
164 | } |
||||
165 | |||||
166 | //Build path |
||||
167 | return |
||||
168 | $base_path.DIRECTORY_SEPARATOR //Base path |
||||
169 | .$this->folder_mapping[get_class($attachment)].DIRECTORY_SEPARATOR.$attachment->getElement()->getID(); |
||||
170 | } |
||||
171 | |||||
172 | /** |
||||
173 | * Handle the submit of an attachment form. |
||||
174 | * This function will move the uploaded file or download the URL file to server, if needed. |
||||
175 | * |
||||
176 | * @param Attachment $attachment the attachment that should be used for handling |
||||
177 | * @param UploadedFile|null $file If given, that file will be moved to the right location |
||||
178 | * @param array $options The options to use with the upload. Here you can specify that an URL should be downloaded, |
||||
179 | * or an file should be moved to a secure location. |
||||
180 | * |
||||
181 | * @return Attachment The attachment with the new filename (same instance as passed $attachment) |
||||
182 | */ |
||||
183 | public function handleFormSubmit(Attachment $attachment, ?UploadedFile $file, array $options = []): Attachment |
||||
184 | { |
||||
185 | $resolver = new OptionsResolver(); |
||||
186 | $this->configureOptions($resolver); |
||||
187 | $options = $resolver->resolve($options); |
||||
188 | |||||
189 | //When a file is given then upload it, otherwise check if we need to download the URL |
||||
190 | if ($file) { |
||||
191 | $this->upload($attachment, $file, $options); |
||||
192 | } elseif ($options['download_url'] && $attachment->isExternal()) { |
||||
193 | $this->downloadURL($attachment, $options); |
||||
194 | } |
||||
195 | |||||
196 | //Move the attachment files to secure location (and back) if needed |
||||
197 | $this->moveFile($attachment, $options['secure_attachment']); |
||||
198 | |||||
199 | //Rename blacklisted (unsecure) files to a better extension |
||||
200 | $this->renameBlacklistedExtensions($attachment); |
||||
201 | |||||
202 | //Check if we should assign this attachment to master picture |
||||
203 | //this is only possible if the attachment is new (not yet persisted to DB) |
||||
204 | if ($options['become_preview_if_empty'] && null === $attachment->getID() && $attachment->isPicture()) { |
||||
205 | $element = $attachment->getElement(); |
||||
206 | if ($element instanceof AttachmentContainingDBElement && null === $element->getMasterPictureAttachment()) { |
||||
207 | $element->setMasterPictureAttachment($attachment); |
||||
208 | } |
||||
209 | } |
||||
210 | |||||
211 | return $attachment; |
||||
212 | } |
||||
213 | |||||
214 | /** |
||||
215 | * Rename attachments with an unsafe extension (meaning files which would be runned by a to a safe one. |
||||
216 | * @param Attachment $attachment |
||||
217 | * @return Attachment |
||||
218 | */ |
||||
219 | protected function renameBlacklistedExtensions(Attachment $attachment): Attachment |
||||
220 | { |
||||
221 | //We can not do anything on builtins or external ressources |
||||
222 | if ($attachment->isBuiltIn() || $attachment->isExternal()) { |
||||
223 | return $attachment; |
||||
224 | } |
||||
225 | |||||
226 | //Determine the old filepath |
||||
227 | $old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); |
||||
228 | if (empty($old_path) || !file_exists($old_path)) { |
||||
229 | return $attachment; |
||||
230 | } |
||||
231 | $filename = basename($old_path); |
||||
232 | $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
233 | |||||
234 | |||||
235 | //Check if the extension is blacklisted and replace the file extension with txt if needed |
||||
236 | if(in_array($ext, self::BLACKLISTED_EXTENSIONS)) { |
||||
237 | $new_path = $this->generateAttachmentPath($attachment, $attachment->isSecure()) |
||||
238 | .DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'txt'); |
||||
239 | |||||
240 | //Move file to new directory |
||||
241 | $fs = new Filesystem(); |
||||
242 | $fs->rename($old_path, $new_path); |
||||
243 | |||||
244 | //Update the attachment |
||||
245 | $attachment->setPath($this->pathResolver->realPathToPlaceholder($new_path)); |
||||
0 ignored issues
–
show
It seems like
$this->pathResolver->rea...oPlaceholder($new_path) can also be of type null ; however, parameter $path of App\Entity\Attachments\Attachment::setPath() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
246 | } |
||||
247 | |||||
248 | |||||
249 | return $attachment; |
||||
250 | } |
||||
251 | |||||
252 | protected function configureOptions(OptionsResolver $resolver): void |
||||
253 | { |
||||
254 | $resolver->setDefaults([ |
||||
255 | //If no preview image was set yet, the new uploaded file will become the preview image |
||||
256 | 'become_preview_if_empty' => true, |
||||
257 | //When an URL is given download the URL |
||||
258 | 'download_url' => false, |
||||
259 | 'secure_attachment' => false, |
||||
260 | ]); |
||||
261 | } |
||||
262 | |||||
263 | /** |
||||
264 | * Move the given attachment to secure location (or back to public folder) if needed. |
||||
265 | * |
||||
266 | * @param Attachment $attachment the attachment for which the file should be moved |
||||
267 | * @param bool $secure_location this value determines, if the attachment is moved to the secure or public folder |
||||
268 | * |
||||
269 | * @return Attachment The attachment with the updated filepath |
||||
270 | */ |
||||
271 | protected function moveFile(Attachment $attachment, bool $secure_location): Attachment |
||||
272 | { |
||||
273 | //We can not do anything on builtins or external ressources |
||||
274 | if ($attachment->isBuiltIn() || $attachment->isExternal()) { |
||||
275 | return $attachment; |
||||
276 | } |
||||
277 | |||||
278 | //Check if we need to move the file |
||||
279 | if ($secure_location === $attachment->isSecure()) { |
||||
280 | return $attachment; |
||||
281 | } |
||||
282 | |||||
283 | //Determine the old filepath |
||||
284 | $old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); |
||||
285 | if (!file_exists($old_path)) { |
||||
0 ignored issues
–
show
It seems like
$old_path can also be of type null ; however, parameter $filename of file_exists() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
286 | return $attachment; |
||||
287 | } |
||||
288 | |||||
289 | $filename = basename($old_path); |
||||
0 ignored issues
–
show
It seems like
$old_path can also be of type null ; however, parameter $path of basename() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
290 | //If the basename is not one of the new unique on, we have to save the old filename |
||||
291 | if (!preg_match('#\w+-\w{13}\.#', $filename)) { |
||||
292 | //Save filename to attachment field |
||||
293 | $attachment->setFilename($attachment->getFilename()); |
||||
294 | } |
||||
295 | |||||
296 | $ext = pathinfo($filename, PATHINFO_EXTENSION); |
||||
297 | $new_path = $this->generateAttachmentPath($attachment, $secure_location) |
||||
298 | .DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, $ext); |
||||
0 ignored issues
–
show
It seems like
$ext can also be of type array ; however, parameter $extension of App\Services\Attachments...ateAttachmentFilename() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
299 | |||||
300 | //Move file to new directory |
||||
301 | $fs = new Filesystem(); |
||||
302 | //Ensure that the new path exists |
||||
303 | $fs->mkdir(dirname($new_path)); |
||||
304 | $fs->rename($old_path, $new_path); |
||||
0 ignored issues
–
show
It seems like
$old_path can also be of type null ; however, parameter $origin of Symfony\Component\Filesystem\Filesystem::rename() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
305 | |||||
306 | //Save info to attachment entity |
||||
307 | $new_path = $this->pathResolver->realPathToPlaceholder($new_path); |
||||
308 | $attachment->setPath($new_path); |
||||
0 ignored issues
–
show
It seems like
$new_path can also be of type null ; however, parameter $path of App\Entity\Attachments\Attachment::setPath() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
309 | |||||
310 | return $attachment; |
||||
311 | } |
||||
312 | |||||
313 | /** |
||||
314 | * Download the URL set in the attachment and save it on the server. |
||||
315 | * |
||||
316 | * @param array $options The options from the handleFormSubmit function |
||||
317 | * |
||||
318 | * @return Attachment The attachment with the new filepath |
||||
319 | */ |
||||
320 | protected function downloadURL(Attachment $attachment, array $options): Attachment |
||||
321 | { |
||||
322 | //Check if we are allowed to download files |
||||
323 | if (!$this->allow_attachments_downloads) { |
||||
324 | throw new RuntimeException('Download of attachments is not allowed!'); |
||||
325 | } |
||||
326 | |||||
327 | $url = $attachment->getURL(); |
||||
328 | |||||
329 | $fs = new Filesystem(); |
||||
330 | $attachment_folder = $this->generateAttachmentPath($attachment, $options['secure_attachment']); |
||||
331 | $tmp_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'tmp'); |
||||
332 | |||||
333 | try { |
||||
334 | $response = $this->httpClient->request('GET', $url, [ |
||||
0 ignored issues
–
show
It seems like
$url can also be of type null ; however, parameter $url of Symfony\Contracts\HttpCl...entInterface::request() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
335 | 'buffer' => false, |
||||
336 | ]); |
||||
337 | |||||
338 | if (200 !== $response->getStatusCode()) { |
||||
339 | throw new AttachmentDownloadException('Status code: '.$response->getStatusCode()); |
||||
340 | } |
||||
341 | |||||
342 | //Open a temporary file in the attachment folder |
||||
343 | $fs->mkdir($attachment_folder); |
||||
344 | $fileHandler = fopen($tmp_path, 'wb'); |
||||
345 | //Write the downloaded data to file |
||||
346 | foreach ($this->httpClient->stream($response) as $chunk) { |
||||
347 | fwrite($fileHandler, $chunk->getContent()); |
||||
348 | } |
||||
349 | fclose($fileHandler); |
||||
350 | |||||
351 | //File download should be finished here, so determine the new filename and extension |
||||
352 | $headers = $response->getHeaders(); |
||||
353 | //Try to determine an filename |
||||
354 | $filename = ''; |
||||
355 | |||||
356 | //If an content disposition header was set try to extract the filename out of it |
||||
357 | if (isset($headers['content-disposition'])) { |
||||
358 | $tmp = []; |
||||
359 | preg_match('/[^;\\n=]*=([\'\"])*(.*)(?(1)\1|)/', $headers['content-disposition'][0], $tmp); |
||||
360 | $filename = $tmp[2]; |
||||
361 | } |
||||
362 | |||||
363 | //If we dont know filename yet, try to determine it out of url |
||||
364 | if ('' === $filename) { |
||||
365 | $filename = basename(parse_url($url, PHP_URL_PATH)); |
||||
366 | } |
||||
367 | |||||
368 | //Set original file |
||||
369 | $attachment->setFilename($filename); |
||||
370 | |||||
371 | //Check if we have a extension given |
||||
372 | $pathinfo = pathinfo($filename); |
||||
373 | if (!empty($pathinfo['extension'])) { |
||||
374 | $new_ext = $pathinfo['extension']; |
||||
375 | } else { //Otherwise we have to guess the extension for the new file, based on its content |
||||
376 | $new_ext = $this->mimeTypes->getExtensions($this->mimeTypes->guessMimeType($tmp_path))[0] ?? 'tmp'; |
||||
0 ignored issues
–
show
It seems like
$this->mimeTypes->guessMimeType($tmp_path) can also be of type null ; however, parameter $mimeType of Symfony\Component\Mime\M...erface::getExtensions() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
377 | } |
||||
378 | |||||
379 | //Rename the file to its new name and save path to attachment entity |
||||
380 | $new_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, $new_ext); |
||||
381 | $fs->rename($tmp_path, $new_path); |
||||
382 | |||||
383 | //Make our file path relative to %BASE% |
||||
384 | $new_path = $this->pathResolver->realPathToPlaceholder($new_path); |
||||
385 | //Save the path to the attachment |
||||
386 | $attachment->setPath($new_path); |
||||
0 ignored issues
–
show
It seems like
$new_path can also be of type null ; however, parameter $path of App\Entity\Attachments\Attachment::setPath() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
387 | } catch (TransportExceptionInterface $transportExceptionInterface) { |
||||
388 | throw new AttachmentDownloadException('Transport error!'); |
||||
389 | } |
||||
390 | |||||
391 | return $attachment; |
||||
392 | } |
||||
393 | |||||
394 | /** |
||||
395 | * Moves the given uploaded file to a permanent place and saves it into the attachment. |
||||
396 | * |
||||
397 | * @param Attachment $attachment The attachment in which the file should be saved |
||||
398 | * @param UploadedFile $file The file which was uploaded |
||||
399 | * @param array $options The options from the handleFormSubmit function |
||||
400 | * |
||||
401 | * @return Attachment The attachment with the new filepath |
||||
402 | */ |
||||
403 | protected function upload(Attachment $attachment, UploadedFile $file, array $options): Attachment |
||||
404 | { |
||||
405 | //Move our temporay attachment to its final location |
||||
406 | $file_path = $file->move( |
||||
407 | $this->generateAttachmentPath($attachment, $options['secure_attachment']), |
||||
408 | $this->generateAttachmentFilename($attachment, $file->getClientOriginalExtension()) |
||||
409 | )->getRealPath(); |
||||
410 | |||||
411 | //Make our file path relative to %BASE% |
||||
412 | $file_path = $this->pathResolver->realPathToPlaceholder($file_path); |
||||
413 | //Save the path to the attachment |
||||
414 | $attachment->setPath($file_path); |
||||
0 ignored issues
–
show
It seems like
$file_path can also be of type null ; however, parameter $path of App\Entity\Attachments\Attachment::setPath() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
415 | //And save original filename |
||||
416 | $attachment->setFilename($file->getClientOriginalName()); |
||||
417 | |||||
418 | return $attachment; |
||||
419 | } |
||||
420 | } |
||||
421 |