Passed
Pull Request — master (#7094)
by
unknown
09:15
created

AdminController::runCleanupTempUploads()   B

Complexity

Conditions 9
Paths 13

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 34
c 0
b 0
f 0
nc 13
nop 2
dl 0
loc 57
rs 8.0555

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller\Admin;
8
9
use Chamilo\CoreBundle\Component\Composer\ScriptHandler;
10
use Chamilo\CoreBundle\Controller\BaseController;
11
use Chamilo\CoreBundle\Entity\Course;
12
use Chamilo\CoreBundle\Entity\ResourceFile;
13
use Chamilo\CoreBundle\Entity\ResourceLink;
14
use Chamilo\CoreBundle\Entity\ResourceType;
15
use Chamilo\CoreBundle\Framework\Container;
16
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
17
use Chamilo\CoreBundle\Helpers\CidReqHelper;
18
use Chamilo\CoreBundle\Helpers\QueryCacheHelper;
19
use Chamilo\CoreBundle\Helpers\TempUploadHelper;
20
use Chamilo\CoreBundle\Helpers\UserHelper;
21
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
22
use Chamilo\CoreBundle\Repository\Node\UserRepository;
23
use Chamilo\CoreBundle\Repository\ResourceFileRepository;
24
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
25
use Chamilo\CoreBundle\Settings\SettingsManager;
26
use Chamilo\CourseBundle\Entity\CDocument;
27
use Doctrine\DBAL\Connection;
28
use Doctrine\ORM\EntityManagerInterface;
29
use Symfony\Component\HttpFoundation\JsonResponse;
30
use Symfony\Component\HttpFoundation\Request;
31
use Symfony\Component\HttpFoundation\Response;
32
use Symfony\Component\Routing\Annotation\Route;
33
use Symfony\Component\Security\Http\Attribute\IsGranted;
34
use Throwable;
35
36
#[Route('/admin')]
37
class AdminController extends BaseController
38
{
39
    private const ITEMS_PER_PAGE = 50;
40
41
    public function __construct(
42
        private readonly ResourceNodeRepository $resourceNodeRepository,
43
        private readonly AccessUrlHelper $accessUrlHelper,
44
        private readonly UserHelper $userHelper,
45
        private readonly CidReqHelper $cidReqHelper
46
    ) {}
47
48
    #[IsGranted('ROLE_ADMIN')]
49
    #[Route('/register-campus', name: 'admin_register_campus', methods: ['POST'])]
50
    public function registerCampus(Request $request, SettingsManager $settingsManager): Response
51
    {
52
        $requestData = $request->toArray();
53
        $doNotListCampus = (bool) $requestData['donotlistcampus'];
54
55
        $settingsManager->setUrl($this->accessUrlHelper->getCurrent());
56
        $settingsManager->updateSetting('platform.registered', 'true');
57
58
        $settingsManager->updateSetting(
59
            'platform.donotlistcampus',
60
            $doNotListCampus ? 'true' : 'false'
61
        );
62
63
        return new Response('', Response::HTTP_NO_CONTENT);
64
    }
65
66
    #[IsGranted('ROLE_ADMIN')]
67
    #[Route('/files_info', name: 'admin_files_info', methods: ['GET'])]
68
    public function listFilesInfo(
69
        Request $request,
70
        ResourceFileRepository $resourceFileRepository,
71
        CourseRepository $courseRepository
72
    ): Response {
73
        $page = $request->query->getInt('page', 1);
74
        $search = $request->query->get('search', '');
75
        $offset = ($page - 1) * self::ITEMS_PER_PAGE;
76
77
        $files = $resourceFileRepository->searchFiles($search, $offset, self::ITEMS_PER_PAGE);
78
        $totalItems = $resourceFileRepository->countFiles($search);
79
        $totalPages = $totalItems > 0 ? (int) ceil($totalItems / self::ITEMS_PER_PAGE) : 1;
80
81
        $fileUrls = [];
82
        $filePaths = [];
83
        $orphanFlags = [];
84
        $linksCount = [];
85
        $coursesByFile = [];
86
87
        foreach ($files as $file) {
88
            $resourceNode = $file->getResourceNode();
89
            $count = 0;
90
            $coursesForThisFile = [];
91
92
            if ($resourceNode) {
93
                $fileUrls[$file->getId()] = $this->resourceNodeRepository->getResourceFileUrl($resourceNode);
94
95
                // Count how many ResourceLinks still point to this node and collect courses.
96
                $links = $resourceNode->getResourceLinks();
97
                if ($links) {
98
                    $count = $links->count();
99
100
                    foreach ($links as $link) {
101
                        $course = $link->getCourse();
102
                        if (!$course) {
103
                            continue;
104
                        }
105
106
                        $courseId = $course->getId();
107
                        // Avoid duplicates for the same course.
108
                        if (!isset($coursesForThisFile[$courseId])) {
109
                            $coursesForThisFile[$courseId] = [
110
                                'id' => $courseId,
111
                                'code' => $course->getCode(),
112
                                'title' => $course->getTitle(),
113
                            ];
114
                        }
115
                    }
116
                }
117
            } else {
118
                $fileUrls[$file->getId()] = null;
119
            }
120
121
            $filePaths[$file->getId()] = '/upload/resource'.$this->resourceNodeRepository->getFilename($file);
122
123
            $linksCount[$file->getId()] = $count;
124
            $orphanFlags[$file->getId()] = 0 === $count;
125
            $coursesByFile[$file->getId()] = array_values($coursesForThisFile);
126
        }
127
128
        // Build course selector options for the "Attach to course" form.
129
        $allCourses = $courseRepository->findBy([], ['title' => 'ASC']);
130
        $courseOptions = [];
131
132
        /** @var Course $course */
133
        foreach ($allCourses as $course) {
134
            $courseOptions[] = [
135
                'id' => $course->getId(),
136
                'code' => $course->getCode(),
137
                'title' => $course->getTitle(),
138
            ];
139
        }
140
141
        return $this->render('@ChamiloCore/Admin/files_info.html.twig', [
142
            'files' => $files,
143
            'fileUrls' => $fileUrls,
144
            'filePaths' => $filePaths,
145
            'totalPages' => $totalPages,
146
            'currentPage' => $page,
147
            'search' => $search,
148
            'orphanFlags' => $orphanFlags,
149
            'linksCount' => $linksCount,
150
            'coursesByFile' => $coursesByFile,
151
            'courseOptions' => $courseOptions,
152
        ]);
153
    }
154
155
    #[IsGranted('ROLE_ADMIN')]
156
    #[Route('/files_info/attach', name: 'admin_files_info_attach', methods: ['POST'])]
157
    public function attachOrphanFileToCourse(
158
        Request $request,
159
        ResourceFileRepository $resourceFileRepository,
160
        CourseRepository $courseRepository,
161
        EntityManagerInterface $em
162
    ): Response {
163
        $token = (string) $request->request->get('_token', '');
164
        if (!$this->isCsrfTokenValid('attach_orphan_file', $token)) {
165
            throw $this->createAccessDeniedException('Invalid CSRF token.');
166
        }
167
168
        $fileId = $request->request->getInt('resource_file_id', 0);
169
        $page = $request->request->getInt('page', 1);
170
        $search = (string) $request->request->get('search', '');
171
172
        if ($fileId <= 0) {
173
            $this->addFlash('error', 'Missing resource file identifier.');
174
175
            return $this->redirectToRoute('admin_files_info', [
176
                'page' => $page,
177
                'search' => $search,
178
            ]);
179
        }
180
181
182
        $courseCodes = [];
183
        $multi = $request->request->all('course_codes');
184
        if (\is_array($multi)) {
185
            foreach ($multi as $code) {
186
                $code = trim((string) $code);
187
                if ('' !== $code) {
188
                    $courseCodes[] = $code;
189
                }
190
            }
191
        }
192
193
        if (0 === \count($courseCodes)) {
194
            $single = $request->request->get('course_code');
195
            $single = null === $single ? '' : trim((string) $single);
196
            if ('' !== $single) {
197
                $courseCodes[] = $single;
198
            }
199
        }
200
201
        // Normalize and remove duplicates.
202
        $courseCodes = array_values(array_unique($courseCodes));
203
204
        if (0 === \count($courseCodes)) {
205
            $this->addFlash('error', 'Please select at least one course.');
206
207
            return $this->redirectToRoute('admin_files_info', [
208
                'page' => $page,
209
                'search' => $search,
210
            ]);
211
        }
212
213
        /** @var ResourceFile|null $resourceFile */
214
        $resourceFile = $resourceFileRepository->find($fileId);
215
        if (!$resourceFile) {
216
            $this->addFlash('error', 'Resource file not found.');
217
218
            return $this->redirectToRoute('admin_files_info', [
219
                'page' => $page,
220
                'search' => $search,
221
            ]);
222
        }
223
224
        $resourceNode = $resourceFile->getResourceNode();
225
        if (!$resourceNode) {
226
            $this->addFlash('error', 'This resource file has no resource node and cannot be attached.');
227
228
            return $this->redirectToRoute('admin_files_info', [
229
                'page' => $page,
230
                'search' => $search,
231
            ]);
232
        }
233
234
        // also create visible documents in the Documents tool.
235
        $createDocuments = (bool) $request->request->get('create_documents', false);
236
237
        // Map existing links by course id to avoid duplicates.
238
        $existingByCourseId = [];
239
        $links = $resourceNode->getResourceLinks();
240
        if ($links) {
0 ignored issues
show
introduced by
$links is of type Doctrine\Common\Collections\Collection, thus it always evaluated to true.
Loading history...
241
            foreach ($links as $existingLink) {
242
                $course = $existingLink->getCourse();
243
                if ($course) {
244
                    $existingByCourseId[$course->getId()] = true;
245
                }
246
            }
247
        }
248
249
        $wasOrphan = 0 === \count($existingByCourseId);
250
        $attachedTitles = [];
251
        $skippedTitles = [];
252
253
        foreach ($courseCodes as $code) {
254
            /** @var Course|null $course */
255
            $course = $courseRepository->findOneBy(['code' => $code]);
256
            if (!$course) {
257
                $skippedTitles[] = \sprintf('%s (not found)', $code);
258
259
                continue;
260
            }
261
262
            $courseId = $course->getId();
263
            if (isset($existingByCourseId[$courseId])) {
264
                // Already attached to this course.
265
                $skippedTitles[] = \sprintf('%s (already attached)', (string) $course->getTitle());
266
267
                continue;
268
            }
269
270
            // If it was orphan, re-parent the node once to the first target course root.
271
            if ($wasOrphan && method_exists($course, 'getResourceNode')) {
272
                $courseRootNode = $course->getResourceNode();
273
                if ($courseRootNode) {
274
                    $resourceNode->setParent($courseRootNode);
275
                }
276
                $wasOrphan = false;
277
            }
278
279
            // Create the ResourceLink for this course.
280
            $link = new ResourceLink();
281
            $link->setResourceNode($resourceNode);
282
            $link->setCourse($course);
283
            $link->setSession(null);
284
285
            $em->persist($link);
286
            $existingByCourseId[$courseId] = true;
287
            $attachedTitles[] = (string) $course->getTitle();
288
289
            // Optional feature: also create a visible document entry for this course.
290
            if ($createDocuments) {
291
                $this->createVisibleDocumentFromResourceFile($resourceFile, $course, $em);
292
            }
293
        }
294
295
        $em->flush();
296
297
        if (!empty($attachedTitles)) {
298
            $this->addFlash(
299
                'success',
300
                \sprintf(
301
                    'File "%s" has been attached to %d course(s): %s.',
302
                    (string) ($resourceFile->getOriginalName() ?? $resourceFile->getTitle() ?? $resourceFile->getId()),
303
                    \count($attachedTitles),
304
                    \implode(', ', $attachedTitles)
305
                )
306
            );
307
        }
308
309
        if (!empty($skippedTitles)) {
310
            $this->addFlash(
311
                'warning',
312
                \sprintf(
313
                    'Some courses were skipped: %s.',
314
                    \implode(', ', $skippedTitles)
315
                )
316
            );
317
        }
318
319
        return $this->redirectToRoute('admin_files_info', [
320
            'page' => $page,
321
            'search' => $search,
322
        ]);
323
    }
324
325
    #[IsGranted('ROLE_ADMIN')]
326
    #[Route('/files_info/detach', name: 'admin_files_info_detach', methods: ['POST'])]
327
    public function detachFileFromCourse(
328
        Request $request,
329
        ResourceFileRepository $resourceFileRepository,
330
        EntityManagerInterface $em
331
    ): Response {
332
        $token = (string) $request->request->get('_token', '');
333
        if (!$this->isCsrfTokenValid('detach_file_from_course', $token)) {
334
            throw $this->createAccessDeniedException('Invalid CSRF token.');
335
        }
336
337
        $fileId = $request->request->getInt('resource_file_id', 0);
338
        $courseId = $request->request->getInt('course_id', 0);
339
        $page = $request->request->getInt('page', 1);
340
        $search = (string) $request->request->get('search', '');
341
342
        if ($fileId <= 0 || $courseId <= 0) {
343
            $this->addFlash('error', 'Missing file or course identifier.');
344
345
            return $this->redirectToRoute('admin_files_info', [
346
                'page' => $page,
347
                'search' => $search,
348
            ]);
349
        }
350
351
        /** @var ResourceFile|null $resourceFile */
352
        $resourceFile = $resourceFileRepository->find($fileId);
353
        if (!$resourceFile) {
354
            $this->addFlash('error', 'Resource file not found.');
355
356
            return $this->redirectToRoute('admin_files_info', [
357
                'page' => $page,
358
                'search' => $search,
359
            ]);
360
        }
361
362
        $resourceNode = $resourceFile->getResourceNode();
363
        if (!$resourceNode) {
364
            $this->addFlash('error', 'This resource file has no resource node and cannot be detached.');
365
366
            return $this->redirectToRoute('admin_files_info', [
367
                'page' => $page,
368
                'search' => $search,
369
            ]);
370
        }
371
372
        $links = $resourceNode->getResourceLinks();
373
        $removed = 0;
374
375
        foreach ($links as $link) {
376
            $course = $link->getCourse();
377
            if ($course && $course->getId() === $courseId) {
378
                $em->remove($link);
379
                ++$removed;
380
            }
381
        }
382
383
        if ($removed > 0) {
384
            $em->flush();
385
386
            $this->addFlash(
387
                'success',
388
                sprintf(
389
                    'File has been detached from %d course link(s).',
390
                    $removed
391
                )
392
            );
393
        } else {
394
            $this->addFlash(
395
                'warning',
396
                'This file is not attached to the selected course.'
397
            );
398
        }
399
400
        return $this->redirectToRoute('admin_files_info', [
401
            'page' => $page,
402
            'search' => $search,
403
        ]);
404
    }
405
406
    #[IsGranted('ROLE_ADMIN')]
407
    #[Route('/files_info/delete', name: 'admin_files_info_delete', methods: ['POST'])]
408
    public function deleteOrphanFile(
409
        Request $request,
410
        ResourceFileRepository $resourceFileRepository,
411
        EntityManagerInterface $em
412
    ): Response {
413
        $token = (string) $request->request->get('_token', '');
414
        if (!$this->isCsrfTokenValid('delete_orphan_file', $token)) {
415
            throw $this->createAccessDeniedException('Invalid CSRF token.');
416
        }
417
418
        $fileId = $request->request->getInt('resource_file_id', 0);
419
        $page = $request->request->getInt('page', 1);
420
        $search = (string) $request->request->get('search', '');
421
422
        if ($fileId <= 0) {
423
            $this->addFlash('error', 'Missing resource file identifier.');
424
425
            return $this->redirectToRoute('admin_files_info', [
426
                'page' => $page,
427
                'search' => $search,
428
            ]);
429
        }
430
431
        $resourceFile = $resourceFileRepository->find($fileId);
432
        if (!$resourceFile) {
433
            $this->addFlash('error', 'Resource file not found.');
434
435
            return $this->redirectToRoute('admin_files_info', [
436
                'page' => $page,
437
                'search' => $search,
438
            ]);
439
        }
440
441
        $resourceNode = $resourceFile->getResourceNode();
442
        $linksCount = $resourceNode ? $resourceNode->getResourceLinks()->count() : 0;
443
        if ($linksCount > 0) {
444
            $this->addFlash('warning', 'This file is still used by at least one course/session and cannot be deleted.');
445
446
            return $this->redirectToRoute('admin_files_info', [
447
                'page' => $page,
448
                'search' => $search,
449
            ]);
450
        }
451
452
        // Compute physical path in var/upload/resource (adapt if you use another directory).
453
        $relativePath = $this->resourceNodeRepository->getFilename($resourceFile);
454
        $storageRoot = $this->getParameter('kernel.project_dir').'/var/upload/resource';
455
        $absolutePath = $storageRoot.$relativePath;
456
457
        if (is_file($absolutePath) && is_writable($absolutePath)) {
458
            @unlink($absolutePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

458
            /** @scrutinizer ignore-unhandled */ @unlink($absolutePath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
459
        }
460
461
        // Optionally remove the resource node as well if it is really orphan.
462
        if ($resourceNode) {
463
            $em->remove($resourceNode);
464
        }
465
466
        $em->remove($resourceFile);
467
        $em->flush();
468
469
        $this->addFlash('success', 'Orphan file and its physical content have been deleted definitively.');
470
471
        return $this->redirectToRoute('admin_files_info', [
472
            'page' => $page,
473
            'search' => $search,
474
        ]);
475
    }
476
477
    #[IsGranted('ROLE_ADMIN')]
478
    #[Route('/resources_info', name: 'admin_resources_info', methods: ['GET'])]
479
    public function listResourcesInfo(
480
        Request $request,
481
        ResourceNodeRepository $resourceNodeRepo,
482
        EntityManagerInterface $em
483
    ): Response {
484
        $resourceTypeId = $request->query->getInt('type');
485
        $resourceTypes = $em->getRepository(ResourceType::class)->findAll();
486
487
        $courses = [];
488
        $showUsers = false;
489
        $typeTitle = null;
490
491
        if ($resourceTypeId > 0) {
492
            /** @var ResourceType|null $rt */
493
            $rt = $em->getRepository(ResourceType::class)->find($resourceTypeId);
494
            $typeTitle = $rt?->getTitle();
495
496
            /** Load ResourceLinks for the selected type */
497
            /** @var ResourceLink[] $resourceLinks */
498
            $resourceLinks = $em->getRepository(ResourceLink::class)->createQueryBuilder('rl')
499
                ->join('rl.resourceNode', 'rn')
500
                ->where('rn.resourceType = :type')
501
                ->setParameter('type', $resourceTypeId)
502
                ->getQuery()
503
                ->getResult()
504
            ;
505
506
            /** Aggregate by course/session key */
507
            $seen = [];
508
            $keysMeta = [];
509
            foreach ($resourceLinks as $link) {
510
                $course = $link->getCourse();
511
                if (!$course) {
512
                    continue;
513
                }
514
                $session = $link->getSession();
515
                $node = $link->getResourceNode();
516
517
                $cid = $course->getId();
518
                $sid = $session?->getId() ?? 0;
519
                $key = self::makeKey($cid, $sid);
520
521
                if (!isset($seen[$key])) {
522
                    $seen[$key] = [
523
                        'type' => $sid ? 'session' : 'course',
524
                        'id' => $sid ?: $cid,
525
                        'courseId' => $cid,
526
                        'sessionId' => $sid,
527
                        'title' => $sid ? ($session->getTitle().' - '.$course->getTitle()) : $course->getTitle(),
528
                        'url' => $sid
529
                            ? '/course/'.$cid.'/home?sid='.$sid
530
                            : '/course/'.$cid.'/home',
531
                        'count' => 0,
532
                        'items' => [],
533
                        'users' => [],
534
                        'firstCreatedAt' => $node->getCreatedAt(),
535
                    ];
536
                    $keysMeta[$key] = ['cid' => $cid, 'sid' => $sid];
537
                }
538
539
                $seen[$key]['count']++;
540
                $seen[$key]['items'][] = $node->getTitle();
541
542
                if ($node->getCreatedAt() < $seen[$key]['firstCreatedAt']) {
543
                    $seen[$key]['firstCreatedAt'] = $node->getCreatedAt();
544
                }
545
            }
546
547
            /* Populate users depending on the resource type */
548
            if (!empty($seen)) {
549
                $usersMap = $this->fetchUsersForType($typeTitle, $em, $keysMeta);
550
                foreach ($usersMap as $key => $names) {
551
                    if (isset($seen[$key]) && $names) {
552
                        $seen[$key]['users'] = array_values(array_unique($names));
553
                    }
554
                }
555
                // Show the "Users" column only if there's any user to display
556
                $showUsers = array_reduce($seen, fn ($acc, $row) => $acc || !empty($row['users']), false);
557
            }
558
559
            /** Normalize output. */
560
            $courses = array_values(array_map(static function ($row) {
561
                $row['items'] = array_values(array_unique($row['items']));
562
563
                return $row;
564
            }, $seen));
565
566
            usort($courses, static fn ($a, $b) => strnatcasecmp($a['title'], $b['title']));
567
        }
568
569
        return $this->render('@ChamiloCore/Admin/resources_info.html.twig', [
570
            'resourceTypes' => $resourceTypes,
571
            'selectedType' => $resourceTypeId,
572
            'courses' => $courses,
573
            'showUsers' => $showUsers,
574
            'typeTitle' => $typeTitle,
575
        ]);
576
    }
577
578
    #[IsGranted('ROLE_ADMIN')]
579
    #[Route('/test-cache-all-users', name: 'chamilo_core_user_test_cache_all_users')]
580
    public function testCacheAllUsers(UserRepository $userRepository): JsonResponse
581
    {
582
        // Without cache
583
        $startNoCache = microtime(true);
584
        $usersNoCache = $userRepository->findAllUsers(false);
585
        $timeNoCache = microtime(true) - $startNoCache;
586
587
        // With cache
588
        $startCache = microtime(true);
589
        $resultCached = $userRepository->findAllUsers(true);
590
        $timeCache = microtime(true) - $startCache;
591
592
        // Check if we have a key (we do if cache was used)
593
        $usersCache = $resultCached['data'] ?? $resultCached;
594
595
        $cacheKey = $resultCached['cache_key'] ?? null;
596
597
        return $this->json([
598
            'without_cache' => [
599
                'count' => \count($usersNoCache),
600
                'execution_time' => $timeNoCache,
601
            ],
602
            'with_cache' => [
603
                'count' => \count($usersCache),
604
                'execution_time' => $timeCache,
605
                'cache_key' => $cacheKey,
606
            ],
607
        ]);
608
    }
609
610
    #[IsGranted('ROLE_ADMIN')]
611
    #[Route(path: '/test-cache-all-users/invalidate', name: 'chamilo_core_user_test_cache_all_users_invalidate')]
612
    public function invalidateCacheAllUsers(QueryCacheHelper $queryCacheHelper): JsonResponse
613
    {
614
        $cacheKey = $queryCacheHelper->getCacheKey('findAllUsers', []);
615
        $queryCacheHelper->invalidate('findAllUsers');
616
617
        return $this->json([
618
            'message' => 'Cache for users invalidated!',
619
            'invalidated_cache_key' => $cacheKey,
620
        ]);
621
    }
622
623
    #[IsGranted('ROLE_ADMIN')]
624
    #[Route('/cleanup-temp-uploads', name: 'admin_cleanup_temp_uploads', methods: ['GET'])]
625
    public function showCleanupTempUploads(
626
        TempUploadHelper $tempUploadHelper,
627
    ): Response {
628
        $stats = $tempUploadHelper->stats(); // ['files' => int, 'bytes' => int]
629
630
        return $this->render('@ChamiloCore/Admin/cleanup_temp_uploads.html.twig', [
631
            'tempDir' => $tempUploadHelper->getTempDir(),
632
            'stats' => $stats,
633
            'defaultOlderThan' => 0, // 0 = delete all
634
        ]);
635
    }
636
637
    #[IsGranted('ROLE_ADMIN')]
638
    #[Route('/cleanup-temp-uploads', name: 'admin_cleanup_temp_uploads_run', methods: ['POST'])]
639
    public function runCleanupTempUploads(
640
        Request $request,
641
        TempUploadHelper $tempUploadHelper,
642
    ): Response {
643
        // CSRF
644
        $token = (string) $request->request->get('_token', '');
645
        if (!$this->isCsrfTokenValid('cleanup_temp_uploads', $token)) {
646
            throw $this->createAccessDeniedException('Invalid CSRF token.');
647
        }
648
649
        // Read inputs
650
        $olderThan = (int) $request->request->get('older_than', 0);
651
        $dryRun = (bool) $request->request->get('dry_run', false);
652
653
        // Purge temp uploads/cache (configurable dir via helper parameter)
654
        $purge = $tempUploadHelper->purge(olderThanMinutes: $olderThan, dryRun: $dryRun);
655
656
        if ($dryRun) {
657
            $this->addFlash('success', \sprintf(
658
                'DRY RUN: %d files (%.2f MB) would be removed from %s.',
659
                $purge['files'],
660
                $purge['bytes'] / 1048576,
661
                $tempUploadHelper->getTempDir()
662
            ));
663
        } else {
664
            $this->addFlash('success', \sprintf(
665
                'Temporary uploads/cache cleaned: %d files removed (%.2f MB) in %s.',
666
                $purge['files'],
667
                $purge['bytes'] / 1048576,
668
                $tempUploadHelper->getTempDir()
669
            ));
670
        }
671
672
        // Remove legacy build main.js and hashed variants
673
        $publicBuild = $this->getParameter('kernel.project_dir').'/public/build';
674
        if (is_dir($publicBuild) && is_readable($publicBuild)) {
675
            @unlink($publicBuild.'/main.js');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

675
            /** @scrutinizer ignore-unhandled */ @unlink($publicBuild.'/main.js');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
676
            $files = @scandir($publicBuild) ?: [];
677
            foreach ($files as $f) {
678
                if (preg_match('/^main\..*\.js$/', $f)) {
679
                    @unlink($publicBuild.'/'.$f);
680
                }
681
            }
682
        }
683
684
        // Rebuild styles/assets like original archive_cleanup.php
685
        try {
686
            ScriptHandler::dumpCssFiles();
687
            $this->addFlash('success', 'The styles and assets in the web/ folder have been refreshed.');
688
        } catch (Throwable $e) {
689
            $this->addFlash('error', 'The styles and assets could not be refreshed. Ensure public/ is writable.');
690
            error_log($e->getMessage());
691
        }
692
693
        return $this->redirectToRoute('admin_cleanup_temp_uploads', [], Response::HTTP_SEE_OTHER);
694
    }
695
696
    /**
697
     * Create a visible CDocument in a course from an existing ResourceFile.
698
     */
699
    private function createVisibleDocumentFromResourceFile(
700
        ResourceFile $resourceFile,
701
        Course $course,
702
        EntityManagerInterface $em
703
    ): void {
704
        $userEntity = $this->userHelper->getCurrent();
705
        if (null === $userEntity) {
706
            return;
707
        }
708
709
        $session = $this->cidReqHelper->getDoctrineSessionEntity();
710
        $group = null;
711
712
        $documentRepo = Container::getDocumentRepository();
713
714
        $parentResource = $course;
715
        $parentNode = $parentResource->getResourceNode();
716
717
        $title = $resourceFile->getTitle()
718
            ?? $resourceFile->getOriginalName()
719
            ?? (string) $resourceFile->getId();
720
721
        $existingDocument = $documentRepo->findCourseResourceByTitle(
722
            $title,
723
            $parentNode,
724
            $course,
725
            $session,
726
            $group
727
        );
728
729
        if (null !== $existingDocument) {
730
            return;
731
        }
732
733
        $document = (new CDocument())
734
            ->setFiletype('file')
735
            ->setTitle($title)
736
            ->setComment(null)
737
            ->setReadonly(false)
738
            ->setCreator($userEntity)
739
            ->setParent($parentResource)
740
            ->addCourseLink($course, $session, $group)
741
        ;
742
743
        $em->persist($document);
744
        $em->flush();
745
746
        $relativePath = $this->resourceNodeRepository->getFilename($resourceFile);
747
        $storageRoot = $this->getParameter('kernel.project_dir').'/var/upload/resource';
748
        $absolutePath = $storageRoot.$relativePath;
749
750
        if (!is_file($absolutePath)) {
751
            return;
752
        }
753
754
        $documentRepo->addFileFromPath($document, $title, $absolutePath);
755
    }
756
757
    /**
758
     * Returns a map key => [user names...] depending on the selected resource type.
759
     *
760
     * @param array<string,array{cid:int,sid:int}> $keysMeta
761
     *
762
     * @return array<string,string[]>
763
     */
764
    private function fetchUsersForType(?string $typeTitle, EntityManagerInterface $em, array $keysMeta): array
765
    {
766
        $type = \is_string($typeTitle) ? strtolower($typeTitle) : '';
767
768
        return match ($type) {
769
            'dropbox' => $this->fetchDropboxRecipients($em, $keysMeta),
770
            // 'student_publications' => $this->fetchStudentPublicationsUsers($em, $keysMeta), // TODO
771
            default => $this->fetchUsersFromResourceLinks($em, $keysMeta),
772
        };
773
    }
774
775
    /**
776
     * Default behavior: list users tied to ResourceLink.user (user-scoped visibility).
777
     *
778
     * @param array<string,array{cid:int,sid:int}> $keysMeta
779
     *
780
     * @return array<string,string[]>
781
     */
782
    private function fetchUsersFromResourceLinks(EntityManagerInterface $em, array $keysMeta): array
783
    {
784
        if (!$keysMeta) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keysMeta of type array<string,array> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
785
            return [];
786
        }
787
788
        // Load resource links having a user and group them by (cid,sid)
789
        $q = $em->createQuery(
790
            'SELECT rl, c, s, u
791
           FROM Chamilo\CoreBundle\Entity\ResourceLink rl
792
           LEFT JOIN rl.course c
793
           LEFT JOIN rl.session s
794
           LEFT JOIN rl.user u
795
          WHERE rl.user IS NOT NULL'
796
        );
797
798
        /** @var ResourceLink[] $links */
799
        $links = $q->getResult();
800
801
        $out = [];
802
        foreach ($links as $rl) {
803
            $cid = $rl->getCourse()?->getId();
804
            if (!$cid) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cid of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
805
                continue;
806
            }
807
            $sid = $rl->getSession()?->getId() ?? 0;
808
            $key = self::makeKey($cid, $sid);
809
            if (!isset($keysMeta[$key])) {
810
                continue; // ignore links not present in the current table
811
            }
812
813
            $name = $rl->getUser()?->getFullName();
814
            if ($name) {
815
                $out[$key][] = $name;
816
            }
817
        }
818
        // Dedupe
819
        foreach ($out as $k => $arr) {
820
            $out[$k] = array_values(array_unique(array_filter($arr)));
821
        }
822
823
        return $out;
824
    }
825
826
    /**
827
     * Dropbox-specific: list real recipients from c_dropbox_person (joined with c_dropbox_file and user).
828
     *
829
     * @param array<string,array{cid:int,sid:int}> $keysMeta
830
     *
831
     * @return array<string,string[]>
832
     */
833
    private function fetchDropboxRecipients(EntityManagerInterface $em, array $keysMeta): array
834
    {
835
        if (!$keysMeta) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keysMeta of type array<string,array> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
836
            return [];
837
        }
838
839
        $cids = array_values(array_unique(array_map(static fn ($m) => (int) $m['cid'], $keysMeta)));
840
        if (!$cids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
841
            return [];
842
        }
843
844
        $conn = $em->getConnection();
845
        $sql = "SELECT
846
                    p.c_id                       AS cid,
847
                    f.session_id                 AS sid,
848
                    CONCAT(u.firstname, ' ', u.lastname) AS uname
849
                FROM c_dropbox_person p
850
                INNER JOIN c_dropbox_file f
851
                        ON f.iid = p.file_id
852
                       AND f.c_id = p.c_id
853
                INNER JOIN `user` u
854
                        ON u.id = p.user_id
855
                WHERE p.c_id IN (:cids)
856
        ";
857
858
        $rows = $conn->executeQuery($sql, ['cids' => $cids], ['cids' => Connection::PARAM_INT_ARRAY])->fetchAllAssociative();
859
860
        $out = [];
861
        foreach ($rows as $r) {
862
            $cid = (int) ($r['cid'] ?? 0);
863
            $sid = (int) ($r['sid'] ?? 0);
864
            $key = self::makeKey($cid, $sid);
865
            if (!isset($keysMeta[$key])) {
866
                continue; // ignore entries not displayed in the table
867
            }
868
            $uname = trim((string) ($r['uname'] ?? ''));
869
            if ('' !== $uname) {
870
                $out[$key][] = $uname;
871
            }
872
        }
873
        // Dedupe
874
        foreach ($out as $k => $arr) {
875
            $out[$k] = array_values(array_unique(array_filter($arr)));
876
        }
877
878
        return $out;
879
    }
880
881
    /**
882
     * Helper to build the aggregation key for course/session rows.
883
     */
884
    private static function makeKey(int $cid, int $sid): string
885
    {
886
        return $sid > 0 ? ('s'.$sid.'-'.$cid) : ('c'.$cid);
887
    }
888
}
889