Passed
Push — master ( 1c618d...c1c6b0 )
by Yannick
10:36 queued 02:52
created

ResourceController::link()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
nc 5
nop 3
dl 0
loc 31
rs 8.9777
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Entity\ResourceLink;
11
use Chamilo\CoreBundle\Entity\ResourceNode;
12
use Chamilo\CoreBundle\Entity\Session;
13
use Chamilo\CoreBundle\Entity\TrackEDownloads;
14
use Chamilo\CoreBundle\Entity\User;
15
use Chamilo\CoreBundle\Repository\ResourceWithLinkInterface;
16
use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceNodeVoter;
17
use Chamilo\CoreBundle\ServiceHelper\UserHelper;
18
use Chamilo\CoreBundle\Tool\ToolChain;
19
use Chamilo\CoreBundle\Traits\ControllerTrait;
20
use Chamilo\CoreBundle\Traits\CourseControllerTrait;
21
use Chamilo\CoreBundle\Traits\GradebookControllerTrait;
22
use Chamilo\CoreBundle\Traits\ResourceControllerTrait;
23
use Chamilo\CourseBundle\Controller\CourseControllerInterface;
24
use Chamilo\CourseBundle\Entity\CTool;
25
use Chamilo\CourseBundle\Repository\CLinkRepository;
26
use Chamilo\CourseBundle\Repository\CShortcutRepository;
27
use Chamilo\CourseBundle\Repository\CToolRepository;
28
use Doctrine\Common\Collections\ArrayCollection;
29
use Doctrine\Common\Collections\Criteria;
30
use Doctrine\ORM\EntityManagerInterface;
31
use Symfony\Bundle\SecurityBundle\Security;
32
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
33
use Symfony\Component\HttpFoundation\JsonResponse;
34
use Symfony\Component\HttpFoundation\RedirectResponse;
35
use Symfony\Component\HttpFoundation\Request;
36
use Symfony\Component\HttpFoundation\Response;
37
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
38
use Symfony\Component\HttpFoundation\StreamedResponse;
39
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
40
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
41
use Symfony\Component\Routing\Attribute\Route;
42
use Symfony\Component\Routing\RouterInterface;
43
use Symfony\Component\Serializer\SerializerInterface;
44
use ZipStream\Option\Archive;
45
use ZipStream\ZipStream;
46
47
/**
48
 * @author Julio Montoya <[email protected]>.
49
 */
50
#[Route('/r')]
51
class ResourceController extends AbstractResourceController implements CourseControllerInterface
52
{
53
    use ControllerTrait;
54
    use CourseControllerTrait;
55
    use GradebookControllerTrait;
56
    use ResourceControllerTrait;
57
58
    public function __construct(
59
        private readonly UserHelper $userHelper,
60
    ) {}
61
62
    #[Route(path: '/{tool}/{type}/{id}/disk_space', methods: ['GET', 'POST'], name: 'chamilo_core_resource_disk_space')]
63
    public function diskSpace(Request $request): Response
64
    {
65
        $nodeId = $request->get('id');
66
        $repository = $this->getRepositoryFromRequest($request);
67
68
        /** @var ResourceNode $resourceNode */
69
        $resourceNode = $repository->getResourceNodeRepository()->find($nodeId);
70
71
        $this->denyAccessUnlessGranted(
72
            ResourceNodeVoter::VIEW,
73
            $resourceNode,
74
            $this->trans('Unauthorised access to resource')
75
        );
76
77
        $course = $this->getCourse();
78
        $totalSize = 0;
79
        if (null !== $course) {
80
            $totalSize = $course->getDiskQuota();
81
        }
82
83
        $size = $repository->getResourceNodeRepository()->getSize(
84
            $resourceNode,
85
            $repository->getResourceType(),
86
            $course
87
        );
88
89
        $labels[] = $course->getTitle();
0 ignored issues
show
Comprehensibility Best Practice introduced by
$labels was never initialized. Although not strictly required by PHP, it is generally a good practice to add $labels = array(); before regardless.
Loading history...
90
        $data[] = $size;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
91
        $sessions = $course->getSessions();
92
93
        foreach ($sessions as $sessionRelCourse) {
94
            $session = $sessionRelCourse->getSession();
95
96
            $labels[] = $course->getTitle().' - '.$session->getTitle();
97
            $size = $repository->getResourceNodeRepository()->getSize(
98
                $resourceNode,
99
                $repository->getResourceType(),
100
                $course,
101
                $session
102
            );
103
            $data[] = $size;
104
        }
105
106
        /*$groups = $course->getGroups();
107
        foreach ($groups as $group) {
108
            $labels[] = $course->getTitle().' - '.$group->getTitle();
109
            $size = $repository->getResourceNodeRepository()->getSize(
110
                $resourceNode,
111
                $repository->getResourceType(),
112
                $course,
113
                null,
114
                $group
115
            );
116
            $data[] = $size;
117
        }*/
118
119
        $used = array_sum($data);
120
        $labels[] = $this->trans('Free');
121
        $data[] = $totalSize - $used;
122
123
        return $this->render(
124
            '@ChamiloCore/Resource/disk_space.html.twig',
125
            [
126
                'resourceNode' => $resourceNode,
127
                'labels' => $labels,
128
                'data' => $data,
129
            ]
130
        );
131
    }
132
133
    /**
134
     * View file of a resource node.
135
     */
136
    #[Route('/{tool}/{type}/{id}/view', name: 'chamilo_core_resource_view', methods: ['GET'])]
137
    public function view(Request $request, EntityManagerInterface $entityManager): Response
138
    {
139
        $id = $request->get('id');
140
        $filter = (string) $request->get('filter'); // See filters definitions in /config/services.yml.
141
        $resourceNode = $this->getResourceNodeRepository()->findOneBy(['uuid' => $id]);
142
143
        if (null === $resourceNode) {
144
            throw new FileNotFoundException($this->trans('Resource not found'));
145
        }
146
147
        $user = $this->userHelper->getCurrent();
148
        $firstResourceLink = $resourceNode->getResourceLinks()->first();
149
        if ($firstResourceLink && $user) {
150
            $resourceLinkId = $firstResourceLink->getId();
151
            $url = $resourceNode->getResourceFile()->getOriginalName();
152
            $downloadRepository = $entityManager->getRepository(TrackEDownloads::class);
153
            $downloadId = $downloadRepository->saveDownload($user->getId(), $resourceLinkId, $url);
154
        }
155
156
        $cid = (int) $request->query->get('cid');
157
        $sid = (int) $request->query->get('sid');
158
        $allUserInfo = null;
159
        if ($cid && $user) {
160
            $allUserInfo = $this->getAllInfoToCertificate(
161
                $user->getId(),
162
                $cid,
163
                $sid,
164
                false
165
            );
166
        }
167
168
        return $this->processFile($request, $resourceNode, 'show', $filter, $allUserInfo);
169
    }
170
171
    /**
172
     * Redirect resource to link.
173
     *
174
     * @return RedirectResponse|void
175
     */
176
    #[Route('/{tool}/{type}/{id}/link', name: 'chamilo_core_resource_link', methods: ['GET'])]
177
    public function link(Request $request, RouterInterface $router, CLinkRepository $cLinkRepository): RedirectResponse
178
    {
179
        $tool = $request->get('tool');
180
        $type = $request->get('type');
181
        $id = $request->get('id');
182
        $resourceNode = $this->getResourceNodeRepository()->find($id);
183
184
        if (null === $resourceNode) {
185
            throw new FileNotFoundException('Resource not found');
186
        }
187
188
        if ('course_tool' === $tool && 'links' === $type) {
189
            $cLink = $cLinkRepository->findOneBy(['resourceNode' => $resourceNode]);
190
            if ($cLink) {
191
                $url = $cLink->getUrl();
192
193
                return $this->redirect($url);
194
            }
195
196
            throw new FileNotFoundException('CLink not found for the given resource node');
197
        } else {
198
            $repo = $this->getRepositoryFromRequest($request);
199
            if ($repo instanceof ResourceWithLinkInterface) {
200
                $resource = $repo->getResourceFromResourceNode($resourceNode->getId());
201
                $url = $repo->getLink($resource, $router, $this->getCourseUrlQueryToArray());
202
203
                return $this->redirect($url);
204
            }
205
206
            $this->abort('No redirect');
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Symfony\Component\HttpFoundation\RedirectResponse. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
207
        }
208
    }
209
210
    /**
211
     * Download file of a resource node.
212
     *
213
     * @return RedirectResponse|StreamedResponse
214
     */
215
    #[Route('/{tool}/{type}/{id}/download', name: 'chamilo_core_resource_download', methods: ['GET'])]
216
    public function download(Request $request, EntityManagerInterface $entityManager)
217
    {
218
        $id = $request->get('id');
219
        $resourceNode = $this->getResourceNodeRepository()->findOneBy(['uuid' => $id]);
220
221
        if (null === $resourceNode) {
222
            throw new FileNotFoundException($this->trans('Resource not found'));
223
        }
224
225
        $repo = $this->getRepositoryFromRequest($request);
226
227
        $this->denyAccessUnlessGranted(
228
            ResourceNodeVoter::VIEW,
229
            $resourceNode,
230
            $this->trans('Unauthorised access to resource')
231
        );
232
233
        // If resource node has a file just download it. Don't download the children.
234
        if ($resourceNode->hasResourceFile()) {
235
            $user = $this->userHelper->getCurrent();
236
            $firstResourceLink = $resourceNode->getResourceLinks()->first();
237
            if ($firstResourceLink) {
238
                $resourceLinkId = $firstResourceLink->getId();
239
                $url = $resourceNode->getResourceFile()->getOriginalName();
240
                $downloadRepository = $entityManager->getRepository(TrackEDownloads::class);
241
                $downloadId = $downloadRepository->saveDownload($user->getId(), $resourceLinkId, $url);
242
            }
243
244
            // Redirect to download single file.
245
            return $this->processFile($request, $resourceNode, 'download');
246
        }
247
248
        $zipName = $resourceNode->getSlug().'.zip';
249
        // $rootNodePath = $resourceNode->getPathForDisplay();
250
        $resourceNodeRepo = $repo->getResourceNodeRepository();
251
        $type = $repo->getResourceType();
252
253
        $criteria = Criteria::create()
254
            ->where(Criteria::expr()->neq('resourceFile', null)) // must have a file
255
            ->andWhere(Criteria::expr()->eq('resourceType', $type)) // only download same type
256
        ;
257
258
        $qb = $resourceNodeRepo->getChildrenQueryBuilder($resourceNode);
259
        $qb->addCriteria($criteria);
260
261
        /** @var ArrayCollection|ResourceNode[] $children */
262
        $children = $qb->getQuery()->getResult();
263
        $count = \count($children);
264
        if (0 === $count) {
265
            $params = $this->getResourceParams($request);
266
            $params['id'] = $id;
267
268
            $this->addFlash('warning', $this->trans('No files'));
269
270
            return $this->redirectToRoute('chamilo_core_resource_list', $params);
271
        }
272
273
        $response = new StreamedResponse(
274
            function () use ($zipName, $children, $repo): void {
275
                // Define suitable options for ZipStream Archive.
276
                $options = new Archive();
277
                $options->setContentType('application/octet-stream');
278
                // initialise zipstream with output zip filename and options.
279
                $zip = new ZipStream($zipName, $options);
280
281
                /** @var ResourceNode $node */
282
                foreach ($children as $node) {
283
                    $stream = $repo->getResourceNodeFileStream($node);
284
                    $fileName = $node->getResourceFile()->getOriginalName();
285
                    // $fileToDisplay = basename($node->getPathForDisplay());
286
                    // $fileToDisplay = str_replace($rootNodePath, '', $node->getPathForDisplay());
287
                    // error_log($fileToDisplay);
288
                    $zip->addFileFromStream($fileName, $stream);
289
                }
290
                $zip->finish();
291
            }
292
        );
293
294
        // Convert the file name to ASCII using iconv
295
        $zipName = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $zipName);
296
297
        $disposition = $response->headers->makeDisposition(
298
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
299
            $zipName // Transliterator::transliterate($zipName)
300
        );
301
        $response->headers->set('Content-Disposition', $disposition);
302
        $response->headers->set('Content-Type', 'application/octet-stream');
303
304
        return $response;
305
    }
306
307
    #[Route('/{tool}/{type}/{id}/change_visibility', name: 'chamilo_core_resource_change_visibility', methods: ['POST'])]
308
    public function changeVisibility(
309
        Request $request,
310
        EntityManagerInterface $entityManager,
311
        SerializerInterface $serializer,
312
        Security $security,
313
    ): Response {
314
        $user = $security->getUser();
315
        $isAdmin = ($user->hasRole('ROLE_SUPER_ADMIN') || $user->hasRole('ROLE_ADMIN'));
316
        $isCourseTeacher = ($user->hasRole('ROLE_CURRENT_COURSE_TEACHER') || $user->hasRole('ROLE_CURRENT_COURSE_SESSION_TEACHER'));
317
318
        if (!($isCourseTeacher || $isAdmin)) {
319
            throw new AccessDeniedHttpException();
320
        }
321
322
        $session = null;
323
        if ($this->getSession()) {
324
            $sessionId = $this->getSession()->getId();
325
            $session = $entityManager->getRepository(Session::class)->find($sessionId);
326
        }
327
        $courseId = $this->getCourse()->getId();
328
        $course = $entityManager->getRepository(Course::class)->find($courseId);
329
        $id = $request->attributes->getInt('id');
330
        $resourceNode = $this->getResourceNodeRepository()->findOneBy(['id' => $id]);
331
332
        if (null === $resourceNode) {
333
            throw new NotFoundHttpException($this->trans('Resource not found'));
334
        }
335
336
        $link = null;
337
        foreach ($resourceNode->getResourceLinks() as $resourceLink) {
338
            if ($resourceLink->getSession() === $session) {
339
                $link = $resourceLink;
340
341
                break;
342
            }
343
        }
344
345
        if (null === $link) {
346
            $link = new ResourceLink();
347
            $link->setResourceNode($resourceNode)
348
                ->setSession($session)
349
                ->setCourse($course)
350
                ->setVisibility(ResourceLink::VISIBILITY_DRAFT)
351
            ;
352
            $entityManager->persist($link);
353
        } else {
354
            if (ResourceLink::VISIBILITY_PUBLISHED === $link->getVisibility()) {
355
                $link->setVisibility(ResourceLink::VISIBILITY_DRAFT);
356
            } else {
357
                $link->setVisibility(ResourceLink::VISIBILITY_PUBLISHED);
358
            }
359
        }
360
361
        $entityManager->flush();
362
363
        $json = $serializer->serialize(
364
            $link,
365
            'json',
366
            [
367
                'groups' => ['ctool:read'],
368
            ]
369
        );
370
371
        return JsonResponse::fromJsonString($json);
372
    }
373
374
    #[Route(
375
        '/{tool}/{type}/change_visibility/{visibility}',
376
        name: 'chamilo_core_resource_change_visibility_all',
377
        methods: ['POST']
378
    )]
379
    public function changeVisibilityAll(
380
        Request $request,
381
        CToolRepository $toolRepository,
382
        CShortcutRepository $shortcutRepository,
383
        ToolChain $toolChain,
384
        EntityManagerInterface $entityManager,
385
        Security $security
386
    ): Response {
387
        $user = $security->getUser();
388
        $isAdmin = ($user->hasRole('ROLE_SUPER_ADMIN') || $user->hasRole('ROLE_ADMIN'));
389
        $isCourseTeacher = ($user->hasRole('ROLE_CURRENT_COURSE_TEACHER') || $user->hasRole('ROLE_CURRENT_COURSE_SESSION_TEACHER'));
390
391
        if (!($isCourseTeacher || $isAdmin)) {
392
            throw new AccessDeniedHttpException();
393
        }
394
395
        $visibility = $request->attributes->get('visibility');
396
397
        $session = null;
398
        if ($this->getSession()) {
399
            $sessionId = $this->getSession()->getId();
400
            $session = $entityManager->getRepository(Session::class)->find($sessionId);
401
        }
402
        $courseId = $this->getCourse()->getId();
403
        $course = $entityManager->getRepository(Course::class)->find($courseId);
404
405
        $result = $toolRepository->getResourcesByCourse($course, $session)
406
            ->addSelect('tool')
407
            ->innerJoin('resource.tool', 'tool')
408
            ->getQuery()
409
            ->getResult()
410
        ;
411
412
        $skipTools = ['course_tool', 'chat', 'notebook', 'wiki'];
413
414
        /** @var CTool $item */
415
        foreach ($result as $item) {
416
            if (\in_array($item->getTitle(), $skipTools, true)) {
417
                continue;
418
            }
419
            $toolModel = $toolChain->getToolFromName($item->getTool()->getTitle());
420
421
            if (!\in_array($toolModel->getCategory(), ['authoring', 'interaction'], true)) {
0 ignored issues
show
Bug introduced by
The method getCategory() does not exist on Chamilo\CoreBundle\Tool\AbstractTool. It seems like you code against a sub-type of said class. However, the method does not exist in Chamilo\CoreBundle\Tool\AbstractCourseTool. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

421
            if (!\in_array($toolModel->/** @scrutinizer ignore-call */ getCategory(), ['authoring', 'interaction'], true)) {
Loading history...
422
                continue;
423
            }
424
425
            $resourceNode = $item->getResourceNode();
426
427
            /** @var ResourceLink $link */
428
            $link = null;
429
            foreach ($resourceNode->getResourceLinks() as $resourceLink) {
430
                if ($resourceLink->getSession() === $session) {
431
                    $link = $resourceLink;
432
433
                    break;
434
                }
435
            }
436
437
            if (null === $link) {
438
                $link = new ResourceLink();
439
                $link->setResourceNode($resourceNode)
440
                    ->setSession($session)
441
                    ->setCourse($course)
442
                    ->setVisibility(ResourceLink::VISIBILITY_DRAFT)
443
                ;
444
                $entityManager->persist($link);
445
            }
446
447
            if ('show' === $visibility) {
448
                $link->setVisibility(ResourceLink::VISIBILITY_PUBLISHED);
449
            } elseif ('hide' === $visibility) {
450
                $link->setVisibility(ResourceLink::VISIBILITY_DRAFT);
451
            }
452
        }
453
454
        $entityManager->flush();
455
456
        return new Response(null, Response::HTTP_NO_CONTENT);
457
    }
458
459
    private function processFile(Request $request, ResourceNode $resourceNode, string $mode = 'show', string $filter = '', ?array $allUserInfo = null): mixed
460
    {
461
        $this->denyAccessUnlessGranted(
462
            ResourceNodeVoter::VIEW,
463
            $resourceNode,
464
            $this->trans('Unauthorised view access to resource')
465
        );
466
467
        $resourceFile = $resourceNode->getResourceFile();
468
469
        if (null === $resourceFile) {
470
            throw new NotFoundHttpException($this->trans('File not found for resource'));
471
        }
472
473
        $fileName = $resourceNode->getResourceFile()->getOriginalName();
474
        $mimeType = $resourceFile->getMimeType();
475
        $resourceNodeRepo = $this->getResourceNodeRepository();
476
477
        switch ($mode) {
478
            case 'download':
479
                $forceDownload = true;
480
481
                break;
482
483
            case 'show':
484
            default:
485
                $forceDownload = false;
486
                // If it's an image then send it to Glide.
487
                if (str_contains($mimeType, 'image')) {
488
                    $glide = $this->getGlide();
489
                    $server = $glide->getServer();
490
                    $params = $request->query->all();
491
492
                    // The filter overwrites the params from GET.
493
                    if (!empty($filter)) {
494
                        $params = $glide->getFilters()[$filter] ?? [];
495
                    }
496
497
                    // The image was cropped manually by the user, so we force to render this version,
498
                    // no matter other crop parameters.
499
                    $crop = $resourceFile->getCrop();
500
                    if (!empty($crop)) {
501
                        $params['crop'] = $crop;
502
                    }
503
504
                    $fileName = $resourceNodeRepo->getFilename($resourceFile);
505
506
                    $response = $server->getImageResponse($fileName, $params);
507
508
                    // Convert the file name to ASCII using iconv
509
                    $fileName = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $fileName);
510
511
                    $disposition = $response->headers->makeDisposition(
512
                        ResponseHeaderBag::DISPOSITION_INLINE,
513
                        basename($fileName)
514
                    );
515
                    $response->headers->set('Content-Disposition', $disposition);
516
517
                    return $response;
518
                }
519
520
                // Modify the HTML content before displaying it.
521
                if (str_contains($mimeType, 'html')) {
522
                    $content = $resourceNodeRepo->getResourceNodeFileContent($resourceNode);
523
524
                    if (null !== $allUserInfo) {
525
                        $tagsToReplace = $allUserInfo[0];
526
                        $replacementValues = $allUserInfo[1];
527
                        $content = str_replace($tagsToReplace, $replacementValues, $content);
528
                    }
529
530
                    // Convert the file name to ASCII using iconv
531
                    $fileName = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $fileName);
532
533
                    $response = new Response();
534
                    $disposition = $response->headers->makeDisposition(
535
                        ResponseHeaderBag::DISPOSITION_INLINE,
536
                        $fileName
537
                    );
538
                    $response->headers->set('Content-Disposition', $disposition);
539
                    $response->headers->set('Content-Type', 'text/html');
540
541
                    // @todo move into a function/class
542
                    if ('true' === $this->getSettingsManager()->getSetting('editor.translate_html')) {
543
                        $user = $this->userHelper->getCurrent();
544
                        if (null !== $user) {
545
                            // Overwrite user_json, otherwise it will be loaded by the TwigListener.php
546
                            $userJson = json_encode(['locale' => $user->getLocale()]);
547
                            $js = $this->renderView(
548
                                '@ChamiloCore/Layout/document.html.twig',
549
                                ['breadcrumb' => '', 'user_json' => $userJson]
550
                            );
551
                            // Insert inside the head tag.
552
                            $content = str_replace('</head>', $js.'</head>', $content);
553
                        }
554
                    }
555
                    if ('true' === $this->getSettingsManager()->getSetting('course.enable_bootstrap_in_documents_html')) {
556
                        // It adds the bootstrap and awesome css
557
                        $links = '<link href="'.api_get_path(WEB_PATH).'libs/bootstrap/bootstrap.min.css" rel="stylesheet">';
558
                        $links .= '<link href="'.api_get_path(WEB_PATH).'libs/bootstrap/font-awesome.min.css" rel="stylesheet">';
559
                        // Insert inside the head tag.
560
                        $content = str_replace('</head>', $links.'</head>', $content);
561
                    }
562
                    $response->setContent($content);
563
564
                    return $response;
565
                }
566
567
                break;
568
        }
569
570
        $stream = $resourceNodeRepo->getResourceNodeFileStream($resourceNode);
571
572
        $response = new StreamedResponse(
573
            function () use ($stream): void {
574
                stream_copy_to_stream($stream, fopen('php://output', 'wb'));
575
            }
576
        );
577
578
        // Convert the file name to ASCII using iconv
579
        $fileName = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $fileName);
580
581
        $disposition = $response->headers->makeDisposition(
582
            $forceDownload ? ResponseHeaderBag::DISPOSITION_ATTACHMENT : ResponseHeaderBag::DISPOSITION_INLINE,
583
            $fileName
584
        );
585
        $response->headers->set('Content-Disposition', $disposition);
586
        $response->headers->set('Content-Type', $mimeType ?: 'application/octet-stream');
587
588
        return $response;
589
    }
590
}
591