Passed
Push — master ( 8e6fd8...1406f5 )
by Angel Fernando Quiroz
07:24 queued 14s
created

ResourceController::streamFileContent()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 17
rs 10
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\User;
14
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
15
use Chamilo\CoreBundle\Repository\ResourceWithLinkInterface;
16
use Chamilo\CoreBundle\Repository\TrackEDownloadsRepository;
17
use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceNodeVoter;
18
use Chamilo\CoreBundle\ServiceHelper\UserHelper;
19
use Chamilo\CoreBundle\Tool\ToolChain;
20
use Chamilo\CoreBundle\Traits\ControllerTrait;
21
use Chamilo\CoreBundle\Traits\CourseControllerTrait;
22
use Chamilo\CoreBundle\Traits\GradebookControllerTrait;
23
use Chamilo\CoreBundle\Traits\ResourceControllerTrait;
24
use Chamilo\CourseBundle\Controller\CourseControllerInterface;
25
use Chamilo\CourseBundle\Entity\CTool;
26
use Chamilo\CourseBundle\Repository\CLinkRepository;
27
use Chamilo\CourseBundle\Repository\CShortcutRepository;
28
use Chamilo\CourseBundle\Repository\CToolRepository;
29
use Doctrine\Common\Collections\ArrayCollection;
30
use Doctrine\Common\Collections\Criteria;
31
use Doctrine\ORM\EntityManagerInterface;
32
use Symfony\Bundle\SecurityBundle\Security;
33
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
34
use Symfony\Component\HttpFoundation\JsonResponse;
35
use Symfony\Component\HttpFoundation\RedirectResponse;
36
use Symfony\Component\HttpFoundation\Request;
37
use Symfony\Component\HttpFoundation\Response;
38
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
39
use Symfony\Component\HttpFoundation\StreamedResponse;
40
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
41
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
42
use Symfony\Component\Routing\Attribute\Route;
43
use Symfony\Component\Routing\RouterInterface;
44
use Symfony\Component\Serializer\SerializerInterface;
45
use ZipStream\Option\Archive;
46
use ZipStream\ZipStream;
47
48
/**
49
 * @author Julio Montoya <[email protected]>.
50
 */
51
#[Route('/r')]
52
class ResourceController extends AbstractResourceController implements CourseControllerInterface
53
{
54
    use ControllerTrait;
55
    use CourseControllerTrait;
56
    use GradebookControllerTrait;
57
    use ResourceControllerTrait;
58
59
    public function __construct(
60
        private readonly UserHelper $userHelper,
61
        private readonly ResourceNodeRepository $resourceNodeRepository,
62
    ) {}
63
64
    #[Route(path: '/{tool}/{type}/{id}/disk_space', methods: ['GET', 'POST'], name: 'chamilo_core_resource_disk_space')]
65
    public function diskSpace(Request $request): Response
66
    {
67
        $nodeId = $request->get('id');
68
        $repository = $this->getRepositoryFromRequest($request);
69
70
        /** @var ResourceNode $resourceNode */
71
        $resourceNode = $repository->getResourceNodeRepository()->find($nodeId);
72
73
        $this->denyAccessUnlessGranted(
74
            ResourceNodeVoter::VIEW,
75
            $resourceNode,
76
            $this->trans('Unauthorised access to resource')
77
        );
78
79
        $course = $this->getCourse();
80
        $totalSize = 0;
81
        if (null !== $course) {
82
            $totalSize = $course->getDiskQuota();
83
        }
84
85
        $size = $repository->getResourceNodeRepository()->getSize(
86
            $resourceNode,
87
            $repository->getResourceType(),
88
            $course
89
        );
90
91
        $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...
92
        $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...
93
        $sessions = $course->getSessions();
94
95
        foreach ($sessions as $sessionRelCourse) {
96
            $session = $sessionRelCourse->getSession();
97
98
            $labels[] = $course->getTitle().' - '.$session->getTitle();
99
            $size = $repository->getResourceNodeRepository()->getSize(
100
                $resourceNode,
101
                $repository->getResourceType(),
102
                $course,
103
                $session
104
            );
105
            $data[] = $size;
106
        }
107
108
        /*$groups = $course->getGroups();
109
        foreach ($groups as $group) {
110
            $labels[] = $course->getTitle().' - '.$group->getTitle();
111
            $size = $repository->getResourceNodeRepository()->getSize(
112
                $resourceNode,
113
                $repository->getResourceType(),
114
                $course,
115
                null,
116
                $group
117
            );
118
            $data[] = $size;
119
        }*/
120
121
        $used = array_sum($data);
122
        $labels[] = $this->trans('Free');
123
        $data[] = $totalSize - $used;
124
125
        return $this->render(
126
            '@ChamiloCore/Resource/disk_space.html.twig',
127
            [
128
                'resourceNode' => $resourceNode,
129
                'labels' => $labels,
130
                'data' => $data,
131
            ]
132
        );
133
    }
134
135
    /**
136
     * View file of a resource node.
137
     */
138
    #[Route('/{tool}/{type}/{id}/view', name: 'chamilo_core_resource_view', methods: ['GET'])]
139
    public function view(Request $request, TrackEDownloadsRepository $trackEDownloadsRepository): Response
140
    {
141
        $id = $request->get('id');
142
        $filter = (string) $request->get('filter'); // See filters definitions in /config/services.yml.
143
        $resourceNode = $this->getResourceNodeRepository()->findOneBy(['uuid' => $id]);
144
145
        if (null === $resourceNode) {
146
            throw new FileNotFoundException($this->trans('Resource not found'));
147
        }
148
149
        $user = $this->userHelper->getCurrent();
150
        $firstResourceLink = $resourceNode->getResourceLinks()->first();
151
        $firstResourceFile = $resourceNode->getResourceFiles()->first();
152
        if ($firstResourceLink && $user && $firstResourceFile) {
153
            $url = $firstResourceFile->getOriginalName();
154
            $trackEDownloadsRepository->saveDownload($user, $firstResourceLink, $url);
155
        }
156
157
        $cid = (int) $request->query->get('cid');
158
        $sid = (int) $request->query->get('sid');
159
        $allUserInfo = null;
160
        if ($cid && $user) {
161
            $allUserInfo = $this->getAllInfoToCertificate(
162
                $user->getId(),
163
                $cid,
164
                $sid,
165
                false
166
            );
167
        }
168
169
        return $this->processFile($request, $resourceNode, 'show', $filter, $allUserInfo);
170
    }
171
172
    /**
173
     * Redirect resource to link.
174
     *
175
     * @return RedirectResponse|void
176
     */
177
    #[Route('/{tool}/{type}/{id}/link', name: 'chamilo_core_resource_link', methods: ['GET'])]
178
    public function link(Request $request, RouterInterface $router, CLinkRepository $cLinkRepository): RedirectResponse
179
    {
180
        $tool = $request->get('tool');
181
        $type = $request->get('type');
182
        $id = $request->get('id');
183
        $resourceNode = $this->getResourceNodeRepository()->find($id);
184
185
        if (null === $resourceNode) {
186
            throw new FileNotFoundException('Resource not found');
187
        }
188
189
        if ('course_tool' === $tool && 'links' === $type) {
190
            $cLink = $cLinkRepository->findOneBy(['resourceNode' => $resourceNode]);
191
            if ($cLink) {
192
                $url = $cLink->getUrl();
193
194
                return $this->redirect($url);
195
            }
196
197
            throw new FileNotFoundException('CLink not found for the given resource node');
198
        } else {
199
            $repo = $this->getRepositoryFromRequest($request);
200
            if ($repo instanceof ResourceWithLinkInterface) {
201
                $resource = $repo->getResourceFromResourceNode($resourceNode->getId());
202
                $url = $repo->getLink($resource, $router, $this->getCourseUrlQueryToArray());
203
204
                return $this->redirect($url);
205
            }
206
207
            $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...
208
        }
209
    }
210
211
    /**
212
     * Download file of a resource node.
213
     */
214
    #[Route('/{tool}/{type}/{id}/download', name: 'chamilo_core_resource_download', methods: ['GET'])]
215
    public function download(Request $request, TrackEDownloadsRepository $trackEDownloadsRepository): Response
216
    {
217
        $id = $request->get('id');
218
        $resourceNode = $this->getResourceNodeRepository()->findOneBy(['uuid' => $id]);
219
220
        if (null === $resourceNode) {
221
            throw new FileNotFoundException($this->trans('Resource not found'));
222
        }
223
224
        $repo = $this->getRepositoryFromRequest($request);
225
226
        $this->denyAccessUnlessGranted(
227
            ResourceNodeVoter::VIEW,
228
            $resourceNode,
229
            $this->trans('Unauthorised access to resource')
230
        );
231
232
        // If resource node has a file just download it. Don't download the children.
233
        if ($resourceNode->hasResourceFile()) {
234
            $user = $this->userHelper->getCurrent();
235
            $firstResourceLink = $resourceNode->getResourceLinks()->first();
236
            if ($firstResourceLink) {
237
                $url = $resourceNode->getResourceFiles()->first()->getOriginalName();
238
                $trackEDownloadsRepository->saveDownload($user, $firstResourceLink, $url);
239
            }
240
241
            // Redirect to download single file.
242
            return $this->processFile($request, $resourceNode, 'download');
243
        }
244
245
        $zipName = $resourceNode->getSlug().'.zip';
246
        // $rootNodePath = $resourceNode->getPathForDisplay();
247
        $resourceNodeRepo = $repo->getResourceNodeRepository();
248
        $type = $repo->getResourceType();
249
250
        $criteria = Criteria::create()
251
            ->where(Criteria::expr()->neq('resourceFiles', null)) // must have a file
252
            ->andWhere(Criteria::expr()->eq('resourceType', $type)) // only download same type
253
        ;
254
255
        $qb = $resourceNodeRepo->getChildrenQueryBuilder($resourceNode);
256
        $qbAlias = $qb->getRootAliases()[0];
257
258
        $qb
259
            ->leftJoin(sprintf('%s.resourceFiles', $qbAlias), 'resourceFiles') // must have a file
260
            ->addCriteria($criteria)
261
        ;
262
263
        /** @var ArrayCollection|ResourceNode[] $children */
264
        $children = $qb->getQuery()->getResult();
265
        $count = \count($children);
266
        if (0 === $count) {
267
            $params = $this->getResourceParams($request);
268
            $params['id'] = $id;
269
270
            $this->addFlash('warning', $this->trans('No files'));
271
272
            return $this->redirectToRoute('chamilo_core_resource_list', $params);
273
        }
274
275
        $response = new StreamedResponse(
276
            function () use ($zipName, $children, $repo): void {
277
                // Define suitable options for ZipStream Archive.
278
                $options = new Archive();
279
                $options->setContentType('application/octet-stream');
280
                // initialise zipstream with output zip filename and options.
281
                $zip = new ZipStream($zipName, $options);
282
283
                /** @var ResourceNode $node */
284
                foreach ($children as $node) {
285
                    $stream = $repo->getResourceNodeFileStream($node);
286
                    $fileName = $node->getResourceFiles()->first()->getOriginalName();
287
                    // $fileToDisplay = basename($node->getPathForDisplay());
288
                    // $fileToDisplay = str_replace($rootNodePath, '', $node->getPathForDisplay());
289
                    // error_log($fileToDisplay);
290
                    $zip->addFileFromStream($fileName, $stream);
291
                }
292
                $zip->finish();
293
            }
294
        );
295
296
        // Convert the file name to ASCII using iconv
297
        $zipName = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $zipName);
298
299
        $disposition = $response->headers->makeDisposition(
300
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
301
            $zipName // Transliterator::transliterate($zipName)
302
        );
303
        $response->headers->set('Content-Disposition', $disposition);
304
        $response->headers->set('Content-Type', 'application/octet-stream');
305
306
        return $response;
307
    }
308
309
    #[Route('/{tool}/{type}/{id}/change_visibility', name: 'chamilo_core_resource_change_visibility', methods: ['POST'])]
310
    public function changeVisibility(
311
        Request $request,
312
        EntityManagerInterface $entityManager,
313
        SerializerInterface $serializer,
314
        Security $security,
315
    ): Response {
316
        $user = $security->getUser();
317
        $isAdmin = ($user->hasRole('ROLE_SUPER_ADMIN') || $user->hasRole('ROLE_ADMIN'));
318
        $isCourseTeacher = ($user->hasRole('ROLE_CURRENT_COURSE_TEACHER') || $user->hasRole('ROLE_CURRENT_COURSE_SESSION_TEACHER'));
319
320
        if (!($isCourseTeacher || $isAdmin)) {
321
            throw new AccessDeniedHttpException();
322
        }
323
324
        $session = null;
325
        if ($this->getSession()) {
326
            $sessionId = $this->getSession()->getId();
327
            $session = $entityManager->getRepository(Session::class)->find($sessionId);
328
        }
329
        $courseId = $this->getCourse()->getId();
330
        $course = $entityManager->getRepository(Course::class)->find($courseId);
331
        $id = $request->attributes->getInt('id');
332
        $resourceNode = $this->getResourceNodeRepository()->findOneBy(['id' => $id]);
333
334
        if (null === $resourceNode) {
335
            throw new NotFoundHttpException($this->trans('Resource not found'));
336
        }
337
338
        $link = null;
339
        foreach ($resourceNode->getResourceLinks() as $resourceLink) {
340
            if ($resourceLink->getSession() === $session) {
341
                $link = $resourceLink;
342
343
                break;
344
            }
345
        }
346
347
        if (null === $link) {
348
            $link = new ResourceLink();
349
            $link->setResourceNode($resourceNode)
350
                ->setSession($session)
351
                ->setCourse($course)
352
                ->setVisibility(ResourceLink::VISIBILITY_DRAFT)
353
            ;
354
            $entityManager->persist($link);
355
        } else {
356
            if (ResourceLink::VISIBILITY_PUBLISHED === $link->getVisibility()) {
357
                $link->setVisibility(ResourceLink::VISIBILITY_DRAFT);
358
            } else {
359
                $link->setVisibility(ResourceLink::VISIBILITY_PUBLISHED);
360
            }
361
        }
362
363
        $entityManager->flush();
364
365
        $json = $serializer->serialize(
366
            $link,
367
            'json',
368
            [
369
                'groups' => ['ctool:read'],
370
            ]
371
        );
372
373
        return JsonResponse::fromJsonString($json);
374
    }
375
376
    #[Route(
377
        '/{tool}/{type}/change_visibility/{visibility}',
378
        name: 'chamilo_core_resource_change_visibility_all',
379
        methods: ['POST']
380
    )]
381
    public function changeVisibilityAll(
382
        Request $request,
383
        CToolRepository $toolRepository,
384
        CShortcutRepository $shortcutRepository,
385
        ToolChain $toolChain,
386
        EntityManagerInterface $entityManager,
387
        Security $security
388
    ): Response {
389
        $user = $security->getUser();
390
        $isAdmin = ($user->hasRole('ROLE_SUPER_ADMIN') || $user->hasRole('ROLE_ADMIN'));
391
        $isCourseTeacher = ($user->hasRole('ROLE_CURRENT_COURSE_TEACHER') || $user->hasRole('ROLE_CURRENT_COURSE_SESSION_TEACHER'));
392
393
        if (!($isCourseTeacher || $isAdmin)) {
394
            throw new AccessDeniedHttpException();
395
        }
396
397
        $visibility = $request->attributes->get('visibility');
398
399
        $session = null;
400
        if ($this->getSession()) {
401
            $sessionId = $this->getSession()->getId();
402
            $session = $entityManager->getRepository(Session::class)->find($sessionId);
403
        }
404
        $courseId = $this->getCourse()->getId();
405
        $course = $entityManager->getRepository(Course::class)->find($courseId);
406
407
        $result = $toolRepository->getResourcesByCourse($course, $session)
408
            ->addSelect('tool')
409
            ->innerJoin('resource.tool', 'tool')
410
            ->getQuery()
411
            ->getResult()
412
        ;
413
414
        $skipTools = ['course_tool', 'chat', 'notebook', 'wiki'];
415
416
        /** @var CTool $item */
417
        foreach ($result as $item) {
418
            if (\in_array($item->getTitle(), $skipTools, true)) {
419
                continue;
420
            }
421
            $toolModel = $toolChain->getToolFromName($item->getTool()->getTitle());
422
423
            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

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