Passed
Pull Request — master (#6795)
by
unknown
09:03
created

AdminController::invalidateCacheAllUsers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 1
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller\Admin;
8
9
use Chamilo\CoreBundle\Component\Composer\ScriptHandler;
10
use Chamilo\CoreBundle\Controller\BaseController;
11
use Chamilo\CoreBundle\Entity\ResourceLink;
12
use Chamilo\CoreBundle\Entity\ResourceType;
13
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
14
use Chamilo\CoreBundle\Helpers\QueryCacheHelper;
15
use Chamilo\CoreBundle\Helpers\TempUploadHelper;
16
use Chamilo\CoreBundle\Repository\Node\UserRepository;
17
use Chamilo\CoreBundle\Repository\ResourceFileRepository;
18
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
19
use Chamilo\CoreBundle\Settings\SettingsManager;
20
use Doctrine\DBAL\Connection;
21
use Doctrine\ORM\EntityManagerInterface;
22
use Symfony\Component\HttpFoundation\JsonResponse;
23
use Symfony\Component\HttpFoundation\Request;
24
use Symfony\Component\HttpFoundation\Response;
25
use Symfony\Component\Routing\Annotation\Route;
26
use Symfony\Component\Security\Http\Attribute\IsGranted;
27
use Throwable;
28
29
#[Route('/admin')]
30
class AdminController extends BaseController
31
{
32
    private const ITEMS_PER_PAGE = 50;
33
34
    public function __construct(
35
        private readonly ResourceNodeRepository $resourceNodeRepository,
36
        private readonly AccessUrlHelper $accessUrlHelper
37
    ) {}
38
39
    #[IsGranted('ROLE_ADMIN')]
40
    #[Route('/register-campus', name: 'admin_register_campus', methods: ['POST'])]
41
    public function registerCampus(Request $request, SettingsManager $settingsManager): Response
42
    {
43
        $requestData = $request->toArray();
44
        $doNotListCampus = (bool) $requestData['donotlistcampus'];
45
46
        $settingsManager->setUrl($this->accessUrlHelper->getCurrent());
47
        $settingsManager->updateSetting('platform.registered', 'true');
48
49
        $settingsManager->updateSetting(
50
            'platform.donotlistcampus',
51
            $doNotListCampus ? 'true' : 'false'
52
        );
53
54
        return new Response('', Response::HTTP_NO_CONTENT);
55
    }
56
57
    #[IsGranted('ROLE_ADMIN')]
58
    #[Route('/files_info', name: 'admin_files_info', methods: ['GET'])]
59
    public function listFilesInfo(Request $request, ResourceFileRepository $resourceFileRepository): Response
60
    {
61
        $page = $request->query->getInt('page', 1);
62
        $search = $request->query->get('search', '');
63
        $offset = ($page - 1) * self::ITEMS_PER_PAGE;
64
65
        $files = $resourceFileRepository->searchFiles($search, $offset, self::ITEMS_PER_PAGE);
66
        $totalItems = $resourceFileRepository->countFiles($search);
67
        $totalPages = $totalItems > 0 ? ceil($totalItems / self::ITEMS_PER_PAGE) : 1;
68
69
        $fileUrls = [];
70
        $filePaths = [];
71
        foreach ($files as $file) {
72
            $resourceNode = $file->getResourceNode();
73
            if ($resourceNode) {
74
                $fileUrls[$file->getId()] = $this->resourceNodeRepository->getResourceFileUrl($resourceNode);
75
                $creator = $resourceNode->getCreator();
76
            } else {
77
                $fileUrls[$file->getId()] = null;
78
                $creator = null;
79
            }
80
            $filePaths[$file->getId()] = '/upload/resource'.$this->resourceNodeRepository->getFilename($file);
81
        }
82
83
        return $this->render('@ChamiloCore/Admin/files_info.html.twig', [
84
            'files' => $files,
85
            'fileUrls' => $fileUrls,
86
            'filePaths' => $filePaths,
87
            'totalPages' => $totalPages,
88
            'currentPage' => $page,
89
            'search' => $search,
90
        ]);
91
    }
92
93
    #[IsGranted('ROLE_ADMIN')]
94
    #[Route('/resources_info', name: 'admin_resources_info', methods: ['GET'])]
95
    public function listResourcesInfo(
96
        Request                $request,
97
        ResourceNodeRepository $resourceNodeRepo,
98
        EntityManagerInterface $em
99
    ): Response
100
    {
101
        $resourceTypeId = $request->query->getInt('type');
102
        $resourceTypes = $em->getRepository(ResourceType::class)->findAll();
103
104
        $courses = [];
105
        $showUsers = false;
106
        $typeTitle = null;
107
108
        if ($resourceTypeId > 0) {
109
            /** @var ResourceType|null $rt */
110
            $rt = $em->getRepository(ResourceType::class)->find($resourceTypeId);
111
            $typeTitle = $rt?->getTitle();
112
113
            /** Load ResourceLinks for the selected type */
114
            /** @var ResourceLink[] $resourceLinks */
115
            $resourceLinks = $em->getRepository(ResourceLink::class)->createQueryBuilder('rl')
116
                ->join('rl.resourceNode', 'rn')
117
                ->where('rn.resourceType = :type')
118
                ->setParameter('type', $resourceTypeId)
119
                ->getQuery()
120
                ->getResult();
121
122
            /** Aggregate by course/session key */
123
            $seen = [];
124
            $keysMeta = [];
125
            foreach ($resourceLinks as $link) {
126
                $course = $link->getCourse();
127
                if (!$course) {
128
                    continue;
129
                }
130
                $session = $link->getSession();
131
                $node = $link->getResourceNode();
132
133
                $cid = $course->getId();
134
                $sid = $session?->getId() ?? 0;
135
                $key = self::makeKey($cid, $sid);
136
137
                if (!isset($seen[$key])) {
138
                    $seen[$key] = [
139
                        'type' => $sid ? 'session' : 'course',
140
                        'id' => $sid ?: $cid,
141
                        'courseId' => $cid,
142
                        'sessionId' => $sid,
143
                        'title' => $sid ? ($session->getTitle() . ' - ' . $course->getTitle()) : $course->getTitle(),
144
                        'url' => $sid
145
                            ? '/course/' . $cid . '/home?sid=' . $sid
146
                            : '/course/' . $cid . '/home',
147
                        'count' => 0,
148
                        'items' => [],
149
                        'users' => [],
150
                        'firstCreatedAt' => $node->getCreatedAt(),
151
                    ];
152
                    $keysMeta[$key] = ['cid' => $cid, 'sid' => $sid];
153
                }
154
155
                $seen[$key]['count']++;
156
                $seen[$key]['items'][] = $node->getTitle();
157
158
                if ($node->getCreatedAt() < $seen[$key]['firstCreatedAt']) {
159
                    $seen[$key]['firstCreatedAt'] = $node->getCreatedAt();
160
                }
161
            }
162
163
            /** Populate users depending on the resource type */
164
            if (!empty($seen)) {
165
                $usersMap = $this->fetchUsersForType($typeTitle, $em, $keysMeta);
166
                foreach ($usersMap as $key => $names) {
167
                    if (isset($seen[$key]) && $names) {
168
                        $seen[$key]['users'] = array_values(array_unique($names));
169
                    }
170
                }
171
                // Show the "Users" column only if there's any user to display
172
                $showUsers = array_reduce($seen, fn($acc, $row) => $acc || !empty($row['users']), false);
173
            }
174
175
            /** Normalize output */
176
            $courses = array_values(array_map(function ($row) {
177
                $row['items'] = array_values(array_unique($row['items']));
178
                return $row;
179
            }, $seen));
180
181
            usort($courses, fn($a, $b) => strnatcasecmp($a['title'], $b['title']));
182
        }
183
184
        return $this->render('@ChamiloCore/Admin/resources_info.html.twig', [
185
            'resourceTypes' => $resourceTypes,
186
            'selectedType' => $resourceTypeId,
187
            'courses' => $courses,
188
            'showUsers' => $showUsers,
189
            'typeTitle' => $typeTitle,
190
        ]);
191
    }
192
193
    #[IsGranted('ROLE_ADMIN')]
194
    #[Route('/test-cache-all-users', name: 'chamilo_core_user_test_cache_all_users')]
195
    public function testCacheAllUsers(UserRepository $userRepository): JsonResponse
196
    {
197
        // Without cache
198
        $startNoCache = microtime(true);
199
        $usersNoCache = $userRepository->findAllUsers(false);
200
        $timeNoCache = microtime(true) - $startNoCache;
201
202
        // With cache
203
        $startCache = microtime(true);
204
        $resultCached = $userRepository->findAllUsers(true);
205
        $timeCache = microtime(true) - $startCache;
206
207
        // Check if we have a key (we do if cache was used)
208
        $usersCache = $resultCached['data'] ?? $resultCached;
209
210
        $cacheKey = $resultCached['cache_key'] ?? null;
211
212
        return $this->json([
213
            'without_cache' => [
214
                'count' => \count($usersNoCache),
215
                'execution_time' => $timeNoCache,
216
            ],
217
            'with_cache' => [
218
                'count' => \count($usersCache),
219
                'execution_time' => $timeCache,
220
                'cache_key' => $cacheKey,
221
            ],
222
        ]);
223
    }
224
225
    #[IsGranted('ROLE_ADMIN')]
226
    #[Route(path: '/test-cache-all-users/invalidate', name: 'chamilo_core_user_test_cache_all_users_invalidate')]
227
    public function invalidateCacheAllUsers(QueryCacheHelper $queryCacheHelper): JsonResponse
228
    {
229
        $cacheKey = $queryCacheHelper->getCacheKey('findAllUsers', []);
230
        $queryCacheHelper->invalidate('findAllUsers');
231
232
        return $this->json([
233
            'message' => 'Cache for users invalidated!',
234
            'invalidated_cache_key' => $cacheKey,
235
        ]);
236
    }
237
238
    #[IsGranted('ROLE_ADMIN')]
239
    #[Route('/cleanup-temp-uploads', name: 'admin_cleanup_temp_uploads', methods: ['GET'])]
240
    public function showCleanupTempUploads(
241
        TempUploadHelper $tempUploadHelper,
242
    ): Response {
243
        $stats = $tempUploadHelper->stats(); // ['files' => int, 'bytes' => int]
244
245
        return $this->render('@ChamiloCore/Admin/cleanup_temp_uploads.html.twig', [
246
            'tempDir' => $tempUploadHelper->getTempDir(),
247
            'stats' => $stats,
248
            'defaultOlderThan' => 0, // 0 = delete all
249
        ]);
250
    }
251
252
    #[IsGranted('ROLE_ADMIN')]
253
    #[Route('/cleanup-temp-uploads', name: 'admin_cleanup_temp_uploads_run', methods: ['POST'])]
254
    public function runCleanupTempUploads(
255
        Request $request,
256
        TempUploadHelper $tempUploadHelper,
257
    ): Response {
258
        // CSRF
259
        $token = (string) $request->request->get('_token', '');
260
        if (!$this->isCsrfTokenValid('cleanup_temp_uploads', $token)) {
261
            throw $this->createAccessDeniedException('Invalid CSRF token.');
262
        }
263
264
        // Read inputs
265
        $olderThan = (int) $request->request->get('older_than', 0);
266
        $dryRun = (bool) $request->request->get('dry_run', false);
267
268
        // Purge temp uploads/cache (configurable dir via helper parameter)
269
        $purge = $tempUploadHelper->purge(olderThanMinutes: $olderThan, dryRun: $dryRun);
270
271
        if ($dryRun) {
272
            $this->addFlash('success', \sprintf(
273
                'DRY RUN: %d files (%.2f MB) would be removed from %s.',
274
                $purge['files'],
275
                $purge['bytes'] / 1048576,
276
                $tempUploadHelper->getTempDir()
277
            ));
278
        } else {
279
            $this->addFlash('success', \sprintf(
280
                'Temporary uploads/cache cleaned: %d files removed (%.2f MB) in %s.',
281
                $purge['files'],
282
                $purge['bytes'] / 1048576,
283
                $tempUploadHelper->getTempDir()
284
            ));
285
        }
286
287
        // Remove legacy build main.js and hashed variants (best effort)
288
        $publicBuild = $this->getParameter('kernel.project_dir').'/public/build';
289
        if (is_dir($publicBuild) && is_readable($publicBuild)) {
290
            @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

290
            /** @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...
291
            $files = @scandir($publicBuild) ?: [];
292
            foreach ($files as $f) {
293
                if (preg_match('/^main\..*\.js$/', $f)) {
294
                    @unlink($publicBuild.'/'.$f);
295
                }
296
            }
297
        }
298
299
        // Rebuild styles/assets like original archive_cleanup.php
300
        try {
301
            ScriptHandler::dumpCssFiles();
302
            $this->addFlash('success', 'The styles and assets in the web/ folder have been refreshed.');
303
        } catch (Throwable $e) {
304
            $this->addFlash('error', 'The styles and assets could not be refreshed. Ensure public/ is writable.');
305
            error_log($e->getMessage());
306
        }
307
308
        return $this->redirectToRoute('admin_cleanup_temp_uploads', [], Response::HTTP_SEE_OTHER);
309
    }
310
311
    /**
312
     * Returns a map key => [user names...] depending on the selected resource type.
313
     *
314
     * @param array<string,array{cid:int,sid:int}> $keysMeta
315
     * @return array<string,string[]>
316
     */
317
    private function fetchUsersForType(?string $typeTitle, EntityManagerInterface $em, array $keysMeta): array
318
    {
319
        $type = is_string($typeTitle) ? strtolower($typeTitle) : '';
320
321
        return match ($type) {
322
            'dropbox' => $this->fetchDropboxRecipients($em, $keysMeta),
323
            // 'student_publications' => $this->fetchStudentPublicationsUsers($em, $keysMeta), // TODO
324
            default => $this->fetchUsersFromResourceLinks($em, $keysMeta),
325
        };
326
    }
327
328
    /**
329
     * Default behavior: list users tied to ResourceLink.user (user-scoped visibility).
330
     *
331
     * @param array<string,array{cid:int,sid:int}> $keysMeta
332
     * @return array<string,string[]>
333
     */
334
    private function fetchUsersFromResourceLinks(EntityManagerInterface $em, array $keysMeta): array
335
    {
336
        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...
337
            return [];
338
        }
339
340
        // Load resource links having a user and group them by (cid,sid)
341
        $q = $em->createQuery(
342
            'SELECT rl, c, s, u
343
           FROM Chamilo\CoreBundle\Entity\ResourceLink rl
344
           LEFT JOIN rl.course c
345
           LEFT JOIN rl.session s
346
           LEFT JOIN rl.user u
347
          WHERE rl.user IS NOT NULL'
348
        );
349
        /** @var ResourceLink[] $links */
350
        $links = $q->getResult();
351
352
        $out = [];
353
        foreach ($links as $rl) {
354
            $cid = $rl->getCourse()?->getId();
355
            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...
356
                continue;
357
            }
358
            $sid = $rl->getSession()?->getId() ?? 0;
359
            $key = self::makeKey($cid, $sid);
360
            if (!isset($keysMeta[$key])) {
361
                continue; // ignore links not present in the current table
362
            }
363
364
            $name = $rl->getUser()?->getFullName();
365
            if ($name) {
366
                $out[$key][] = $name;
367
            }
368
        }
369
        // Dedupe
370
        foreach ($out as $k => $arr) {
371
            $out[$k] = array_values(array_unique(array_filter($arr)));
372
        }
373
        return $out;
374
    }
375
376
    /**
377
     * Dropbox-specific: list real recipients from c_dropbox_person (joined with c_dropbox_file and user).
378
     *
379
     * @param array<string,array{cid:int,sid:int}> $keysMeta
380
     * @return array<string,string[]>
381
     */
382
    private function fetchDropboxRecipients(EntityManagerInterface $em, array $keysMeta): array
383
    {
384
        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...
385
            return [];
386
        }
387
388
        $cids = array_values(array_unique(array_map(fn($m) => (int) $m['cid'], $keysMeta)));
389
        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...
390
            return [];
391
        }
392
393
        $conn = $em->getConnection();
394
        $sql = "SELECT
395
                    p.c_id                       AS cid,
396
                    f.session_id                 AS sid,
397
                    CONCAT(u.firstname, ' ', u.lastname) AS uname
398
                FROM c_dropbox_person p
399
                INNER JOIN c_dropbox_file f
400
                        ON f.iid = p.file_id
401
                       AND f.c_id = p.c_id
402
                INNER JOIN `user` u
403
                        ON u.id = p.user_id
404
                WHERE p.c_id IN (:cids)
405
        ";
406
407
        $rows = $conn->executeQuery($sql, ['cids' => $cids], ['cids' => Connection::PARAM_INT_ARRAY])->fetchAllAssociative();
408
409
        $out = [];
410
        foreach ($rows as $r) {
411
            $cid = (int) ($r['cid'] ?? 0);
412
            $sid = (int) ($r['sid'] ?? 0);
413
            $key = self::makeKey($cid, $sid);
414
            if (!isset($keysMeta[$key])) {
415
                continue; // ignore entries not displayed in the table
416
            }
417
            $uname = trim((string) ($r['uname'] ?? ''));
418
            if ($uname !== '') {
419
                $out[$key][] = $uname;
420
            }
421
        }
422
        // Dedupe
423
        foreach ($out as $k => $arr) {
424
            $out[$k] = array_values(array_unique(array_filter($arr)));
425
        }
426
        return $out;
427
    }
428
429
    /** Helper to build the aggregation key for course/session rows. */
430
    private static function makeKey(int $cid, int $sid): string
431
    {
432
        return $sid > 0 ? ('s' . $sid . '-' . $cid) : ('c' . $cid);
433
    }
434
}
435