Passed
Pull Request — master (#7139)
by
unknown
11:28
created

createVisibleDocumentFromResourceFile()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 81
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 45
c 0
b 0
f 0
nc 10
nop 3
dl 0
loc 81
rs 8.2666

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

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

707
            /** @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...
708
            $files = @scandir($publicBuild) ?: [];
709
            foreach ($files as $f) {
710
                if (preg_match('/^main\..*\.js$/', $f)) {
711
                    @unlink($publicBuild.'/'.$f);
712
                }
713
            }
714
        }
715
716
        // Rebuild styles/assets like original archive_cleanup.php
717
        try {
718
            ScriptHandler::dumpCssFiles();
719
            $this->addFlash('success', 'The styles and assets in the web/ folder have been refreshed.');
720
        } catch (Throwable $e) {
721
            $this->addFlash('error', 'The styles and assets could not be refreshed. Ensure public/ is writable.');
722
            error_log($e->getMessage());
723
        }
724
725
        return $this->redirectToRoute('admin_cleanup_temp_uploads', [], Response::HTTP_SEE_OTHER);
726
    }
727
728
    /**
729
     * Create a visible CDocument in a course from an existing ResourceFile.
730
     */
731
    private function createVisibleDocumentFromResourceFile(
732
        ResourceFile $resourceFile,
733
        Course $course,
734
        EntityManagerInterface $em
735
    ): CDocument {
736
        $userEntity = $this->userHelper->getCurrent();
737
        if (null === $userEntity) {
738
            throw new \RuntimeException('Current user is required to create or reuse a document.');
739
        }
740
741
        // Current node (may be null for truly orphan files).
742
        $resourceNode = $resourceFile->getResourceNode();
743
744
        if (null === $resourceNode) {
745
            $courseRootNode = $course->getResourceNode();
746
            if (null === $courseRootNode) {
747
                throw new \RuntimeException('Course root node is required to attach a resource node.');
748
            }
749
750
            // Create the node that will be shared by all courses.
751
            $resourceNode = new ResourceNode();
752
            $resourceNode
753
                ->setCreator($userEntity)
754
                ->setTitle(
755
                    $resourceFile->getOriginalName()
756
                    ?? $resourceFile->getTitle()
757
                    ?? (string) $resourceFile->getId()
758
                )
759
                ->setParent($courseRootNode)
760
                ->setResourceType(
761
                    $this->resourceNodeRepository->getResourceTypeForClass(CDocument::class)
762
                )
763
            ;
764
765
            // Link file <-> node so getFirstResourceFile() returns this file.
766
            $resourceNode->addResourceFile($resourceFile);
767
            $resourceFile->setResourceNode($resourceNode);
768
769
            $em->persist($resourceNode);
770
            $em->persist($resourceFile);
771
        }
772
773
        $documentRepo = Container::getDocumentRepository();
774
775
        /** @var CDocument|null $document */
776
        $document = $documentRepo->findOneBy([
777
            'resourceNode' => $resourceNode,
778
        ]);
779
780
        if (null === $document) {
781
            $title = $resourceFile->getOriginalName()
782
                ?? $resourceFile->getTitle()
783
                ?? (string) $resourceFile->getId();
784
785
            $document = (new CDocument())
786
                ->setFiletype('file')
787
                ->setTitle($title)
788
                ->setComment(null)
789
                ->setReadonly(false)
790
                ->setTemplate(false)
791
                ->setCreator($userEntity)
792
                // First course becomes the logical owner. The node is shared.
793
                ->setParent($course)
794
                ->setResourceNode($resourceNode)
795
            ;
796
797
            $em->persist($document);
798
        }
799
800
        // IMPORTANT: Always ensure the file is linked to the document's node,
801
        // even if it already had a resource node.
802
        $documentNode = $document->getResourceNode();
803
        if (null !== $documentNode && $resourceFile->getResourceNode() !== $documentNode) {
804
            // Move the file to the shared document node used by the document.
805
            $documentNode->addResourceFile($resourceFile);
806
            $resourceFile->setResourceNode($documentNode);
807
            $em->persist($resourceFile);
808
        }
809
810
        // Do NOT create course links or flush here: this is handled by the caller.
811
        return $document;
812
    }
813
814
    /**
815
     * Returns a map key => [user names...] depending on the selected resource type.
816
     *
817
     * @param array<string,array{cid:int,sid:int}> $keysMeta
818
     *
819
     * @return array<string,string[]>
820
     */
821
    private function fetchUsersForType(?string $typeTitle, EntityManagerInterface $em, array $keysMeta): array
822
    {
823
        $type = \is_string($typeTitle) ? strtolower($typeTitle) : '';
824
825
        return match ($type) {
826
            'dropbox' => $this->fetchDropboxRecipients($em, $keysMeta),
827
            // 'student_publications' => $this->fetchStudentPublicationsUsers($em, $keysMeta), // TODO
828
            default => $this->fetchUsersFromResourceLinks($em, $keysMeta),
829
        };
830
    }
831
832
    /**
833
     * Default behavior: list users tied to ResourceLink.user (user-scoped visibility).
834
     *
835
     * @param array<string,array{cid:int,sid:int}> $keysMeta
836
     *
837
     * @return array<string,string[]>
838
     */
839
    private function fetchUsersFromResourceLinks(EntityManagerInterface $em, array $keysMeta): array
840
    {
841
        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...
842
            return [];
843
        }
844
845
        // Load resource links having a user and group them by (cid,sid)
846
        $q = $em->createQuery(
847
            'SELECT rl, c, s, u
848
           FROM Chamilo\CoreBundle\Entity\ResourceLink rl
849
           LEFT JOIN rl.course c
850
           LEFT JOIN rl.session s
851
           LEFT JOIN rl.user u
852
          WHERE rl.user IS NOT NULL'
853
        );
854
855
        /** @var ResourceLink[] $links */
856
        $links = $q->getResult();
857
858
        $out = [];
859
        foreach ($links as $rl) {
860
            $cid = $rl->getCourse()?->getId();
861
            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...
862
                continue;
863
            }
864
            $sid = $rl->getSession()?->getId() ?? 0;
865
            $key = self::makeKey($cid, $sid);
866
            if (!isset($keysMeta[$key])) {
867
                continue; // ignore links not present in the current table
868
            }
869
870
            $name = $rl->getUser()?->getFullName();
871
            if ($name) {
872
                $out[$key][] = $name;
873
            }
874
        }
875
        // Dedupe
876
        foreach ($out as $k => $arr) {
877
            $out[$k] = array_values(array_unique(array_filter($arr)));
878
        }
879
880
        return $out;
881
    }
882
883
    /**
884
     * Dropbox-specific: list real recipients from c_dropbox_person (joined with c_dropbox_file and user).
885
     *
886
     * @param array<string,array{cid:int,sid:int}> $keysMeta
887
     *
888
     * @return array<string,string[]>
889
     */
890
    private function fetchDropboxRecipients(EntityManagerInterface $em, array $keysMeta): array
891
    {
892
        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...
893
            return [];
894
        }
895
896
        $cids = array_values(array_unique(array_map(static fn ($m) => (int) $m['cid'], $keysMeta)));
897
        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...
898
            return [];
899
        }
900
901
        $conn = $em->getConnection();
902
        $sql = "SELECT
903
                    p.c_id                       AS cid,
904
                    f.session_id                 AS sid,
905
                    CONCAT(u.firstname, ' ', u.lastname) AS uname
906
                FROM c_dropbox_person p
907
                INNER JOIN c_dropbox_file f
908
                        ON f.iid = p.file_id
909
                       AND f.c_id = p.c_id
910
                INNER JOIN `user` u
911
                        ON u.id = p.user_id
912
                WHERE p.c_id IN (:cids)
913
        ";
914
915
        $rows = $conn->executeQuery($sql, ['cids' => $cids], ['cids' => Connection::PARAM_INT_ARRAY])->fetchAllAssociative();
916
917
        $out = [];
918
        foreach ($rows as $r) {
919
            $cid = (int) ($r['cid'] ?? 0);
920
            $sid = (int) ($r['sid'] ?? 0);
921
            $key = self::makeKey($cid, $sid);
922
            if (!isset($keysMeta[$key])) {
923
                continue; // ignore entries not displayed in the table
924
            }
925
            $uname = trim((string) ($r['uname'] ?? ''));
926
            if ('' !== $uname) {
927
                $out[$key][] = $uname;
928
            }
929
        }
930
        // Dedupe
931
        foreach ($out as $k => $arr) {
932
            $out[$k] = array_values(array_unique(array_filter($arr)));
933
        }
934
935
        return $out;
936
    }
937
938
    /**
939
     * Helper to build the aggregation key for course/session rows.
940
     */
941
    private static function makeKey(int $cid, int $sid): string
942
    {
943
        return $sid > 0 ? ('s'.$sid.'-'.$cid) : ('c'.$cid);
944
    }
945
}
946