Passed
Pull Request — master (#6894)
by
unknown
09:02
created

CDocumentRepository::safeTitle()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CourseBundle\Repository;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Entity\ResourceFile;
11
use Chamilo\CoreBundle\Entity\ResourceLink;
12
use Chamilo\CoreBundle\Entity\ResourceNode;
13
use Chamilo\CoreBundle\Entity\Session;
14
use Chamilo\CoreBundle\Entity\User;
15
use Chamilo\CoreBundle\Framework\Container;
16
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
17
use Chamilo\CoreBundle\Repository\ResourceRepository;
18
use Chamilo\CourseBundle\Entity\CDocument;
19
use Chamilo\CourseBundle\Entity\CGroup;
20
use Chamilo\CourseBundle\Entity\CLp;
21
use Doctrine\ORM\EntityManagerInterface;
22
use Doctrine\ORM\QueryBuilder;
23
use Doctrine\Persistence\ManagerRegistry;
24
use RuntimeException;
25
use Symfony\Component\HttpFoundation\File\UploadedFile;
26
27
final class CDocumentRepository extends ResourceRepository
28
{
29
    public function __construct(ManagerRegistry $registry)
30
    {
31
        parent::__construct($registry, CDocument::class);
32
    }
33
34
    public function getParent(CDocument $document): ?CDocument
35
    {
36
        $resourceParent = $document->getResourceNode()->getParent();
37
38
        if (null !== $resourceParent) {
39
            return $this->findOneBy(['resourceNode' => $resourceParent->getId()]);
40
        }
41
42
        return null;
43
    }
44
45
    public function getFolderSize(ResourceNode $resourceNode, Course $course, ?Session $session = null): int
46
    {
47
        return $this->getResourceNodeRepository()->getSize($resourceNode, $this->getResourceType(), $course, $session);
48
    }
49
50
    /**
51
     * @return CDocument[]
52
     */
53
    public function findDocumentsByAuthor(int $userId)
54
    {
55
        $qb = $this->createQueryBuilder('d');
56
57
        return $qb
58
            ->innerJoin('d.resourceNode', 'node')
59
            ->innerJoin('node.resourceLinks', 'l')
60
            ->where('l.user = :user')
61
            ->setParameter('user', $userId)
62
            ->getQuery()
63
            ->getResult()
64
        ;
65
    }
66
67
    public function countUserDocuments(User $user, Course $course, ?Session $session = null, ?CGroup $group = null): int
68
    {
69
        $qb = $this->getResourcesByCourseLinkedToUser($user, $course, $session, $group);
70
        $qb->select('count(resource)');
71
        $this->addFileTypeQueryBuilder('file', $qb);
72
73
        return $this->getCount($qb);
74
    }
75
76
    protected function addFileTypeQueryBuilder(string $fileType, ?QueryBuilder $qb = null): QueryBuilder
77
    {
78
        $qb = $this->getOrCreateQueryBuilder($qb);
79
80
        return $qb
81
            ->andWhere('resource.filetype = :filetype')
82
            ->setParameter('filetype', $fileType)
83
        ;
84
    }
85
86
    /**
87
     * Register the original SCORM ZIP under:
88
     *   <course root> / Learning paths / SCORM - {lp_id} - {lp_title} / {lp_title}.zip
89
     */
90
    public function registerScormZip(Course $course, ?Session $session, CLp $lp, UploadedFile $zip): void
91
    {
92
        $em = $this->em();
93
94
        // Ensure "Learning paths" directly under the course resource node
95
        $lpTop = $this->ensureLearningPathSystemFolder($course, $session);
96
97
        // Subfolder per LP
98
        $lpFolderTitle = \sprintf('SCORM - %d - %s', $lp->getIid(), $this->safeTitle($lp->getTitle()));
99
        $lpFolder = $this->ensureFolder(
100
            $course,
101
            $lpTop,
102
            $lpFolderTitle,
103
            ResourceLink::VISIBILITY_DRAFT,
104
            $session
105
        );
106
107
        // ZIP file under the LP folder
108
        $this->createFileInFolder(
109
            $course,
110
            $lpFolder,
111
            $zip,
112
            \sprintf('SCORM ZIP for LP #%d', $lp->getIid()),
113
            ResourceLink::VISIBILITY_DRAFT,
114
            $session
115
        );
116
117
        $em->flush();
118
    }
119
120
    /**
121
     * Remove the LP subfolder "SCORM - {lp_id} - ..." under "Learning paths".
122
     */
123
    public function purgeScormZip(Course $course, CLp $lp): void
124
    {
125
        $em = $this->em();
126
        $prefix = \sprintf('SCORM - %d - ', $lp->getIid());
127
128
        $courseRoot = $course->getResourceNode();
129
        if ($courseRoot) {
130
            // SCORM folder directly under course root
131
            if ($this->tryDeleteFirstFolderByTitlePrefix($courseRoot, $prefix)) {
132
                $em->flush();
133
134
                return;
135
            }
136
137
            // Or under "Learning paths"
138
            $lpTop = $this->findChildNodeByTitle($courseRoot, 'Learning paths');
139
            if ($lpTop && $this->tryDeleteFirstFolderByTitlePrefix($lpTop, $prefix)) {
140
                $em->flush();
141
142
                return;
143
            }
144
        }
145
    }
146
147
    /**
148
     * Try to delete the first child folder whose title starts with $prefix under $parent.
149
     * Returns true if something was removed.
150
     */
151
    private function tryDeleteFirstFolderByTitlePrefix(ResourceNode $parent, string $prefix): bool
152
    {
153
        $em = $this->em();
154
        $qb = $em->createQueryBuilder()
155
            ->select('rn')
156
            ->from(ResourceNode::class, 'rn')
157
            ->where('rn.parent = :parent')
158
            ->andWhere('rn.title LIKE :prefix')
159
            ->setParameters(['parent' => $parent, 'prefix' => $prefix.'%'])
160
            ->setMaxResults(1)
161
        ;
162
163
        /** @var ResourceNode|null $node */
164
        $node = $qb->getQuery()->getOneOrNullResult();
165
        if ($node) {
166
            $em->remove($node);
167
168
            return true;
169
        }
170
171
        return false;
172
    }
173
174
    /**
175
     * Find the course Documents root node.
176
     *
177
     * Primary: parent = course.resourceNode
178
     * Fallback (legacy): parent IS NULL
179
     */
180
    public function getCourseDocumentsRootNode(Course $course): ?ResourceNode
181
    {
182
        $em = $this->em();
183
        $rt = $this->getResourceType();
184
        $courseNode = $course->getResourceNode();
185
186
        if ($courseNode) {
187
            $node = $em->createQuery(
188
                'SELECT rn
189
                   FROM Chamilo\CoreBundle\Entity\ResourceNode rn
190
                   JOIN rn.resourceType rt
191
                   JOIN rn.resourceLinks rl
192
                  WHERE rn.parent = :parent
193
                    AND rt = :rtype
194
                    AND rl.course = :course
195
               ORDER BY rn.id ASC'
196
            )
197
                ->setParameters([
198
                    'parent' => $courseNode,
199
                    'rtype' => $rt,
200
                    'course' => $course,
201
                ])
202
                ->setMaxResults(1)
203
                ->getOneOrNullResult()
204
            ;
205
206
            if ($node) {
207
                return $node;
208
            }
209
        }
210
211
        // Fallback for historical data (Documents root directly under NULL)
212
        return $em->createQuery(
213
            'SELECT rn
214
               FROM Chamilo\CoreBundle\Entity\ResourceNode rn
215
               JOIN rn.resourceType rt
216
               JOIN rn.resourceLinks rl
217
              WHERE rn.parent IS NULL
218
                AND rt = :rtype
219
                AND rl.course = :course
220
           ORDER BY rn.id ASC'
221
        )
222
            ->setParameters(['rtype' => $rt, 'course' => $course])
223
            ->setMaxResults(1)
224
            ->getOneOrNullResult()
225
        ;
226
    }
227
228
    /**
229
     * Ensure the course "Documents" root node exists.
230
     * Now parent = course.resourceNode (so the tool lists it under the course).
231
     */
232
    public function ensureCourseDocumentsRootNode(Course $course): ResourceNode
233
    {
234
        if ($root = $this->getCourseDocumentsRootNode($course)) {
235
            return $root;
236
        }
237
238
        $em = $this->em();
239
        $type = $this->getResourceType();
240
241
        /** @var User|null $user */
242
        $user = api_get_user_entity();
243
244
        $node = new ResourceNode();
245
        $node->setTitle('Documents');
246
        $node->setResourceType($type);
247
        $node->setPublic(false);
248
249
        if ($course->getResourceNode()) {
250
            $node->setParent($course->getResourceNode());
251
        }
252
253
        if ($user) {
254
            $node->setCreator($user);
255
        }
256
257
        $link = new ResourceLink();
258
        $link->setCourse($course);
259
        $link->setVisibility(ResourceLink::VISIBILITY_PUBLISHED);
260
        $node->addResourceLink($link);
261
262
        $em->persist($node);
263
        $em->flush();
264
265
        return $node;
266
    }
267
268
    /**
269
     * Create (if missing) a folder under $parent using the CDocument API path.
270
     * The folder is attached under the given $parent (not the course root),
271
     * and linked to the course (cid) and optionally the session (sid).
272
     */
273
    public function ensureFolder(
274
        Course $course,
275
        ResourceNode $parent,
276
        string $folderTitle,
277
        int $visibility = ResourceLink::VISIBILITY_DRAFT,
278
        ?Session $session = null
279
    ): ResourceNode {
280
        // Return if a child with this title already exists under the parent
281
        if ($child = $this->findChildNodeByTitle($parent, $folderTitle)) {
282
            return $child;
283
        }
284
285
        /** @var User|null $user */
286
        $user = api_get_user_entity();
287
288
        $doc = new CDocument();
289
        $doc->setTitle($folderTitle);
290
        $doc->setFiletype('folder');
291
292
        // IMPORTANT: attach to the given parent, not to the course root
293
        $doc->setParentResourceNode($parent->getId());
294
295
        // Link to course (and optional session)
296
        $link = [
297
            'cid' => $course->getId(),
298
            'visibility' => $visibility,
299
        ];
300
        if ($session && method_exists($session, 'getId')) {
301
            $link['sid'] = $session->getId();
302
        }
303
        $doc->setResourceLinkArray([$link]);
304
305
        if ($user) {
306
            $doc->setCreator($user);
307
        }
308
309
        $em = $this->em();
310
        $em->persist($doc);
311
        $em->flush();
312
313
        return $doc->getResourceNode();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $doc->getResourceNode() could return the type null which is incompatible with the type-hinted return Chamilo\CoreBundle\Entity\ResourceNode. Consider adding an additional type-check to rule them out.
Loading history...
314
    }
315
316
    /**
317
     * Create a file under $parent using the CDocument API path.
318
     * The file is linked to the course (cid) and optionally the session (sid).
319
     */
320
    public function createFileInFolder(
321
        Course $course,
322
        ResourceNode $parent,
323
        UploadedFile $uploaded,
324
        string $comment,
325
        int $visibility,
326
        ?Session $session = null
327
    ): ResourceNode {
328
        /** @var User|null $user */
329
        $user = api_get_user_entity();
330
331
        $title = $uploaded->getClientOriginalName();
332
333
        $doc = new CDocument();
334
        $doc->setTitle($title);
335
        $doc->setFiletype('file');
336
        $doc->setComment($comment);
337
        $doc->setParentResourceNode($parent->getId());
338
339
        $link = [
340
            'cid' => $course->getId(),
341
            'visibility' => $visibility,
342
        ];
343
        if ($session && method_exists($session, 'getId')) {
344
            $link['sid'] = $session->getId();
345
        }
346
        $doc->setResourceLinkArray([$link]);
347
348
        $doc->setUploadFile($uploaded);
349
350
        if ($user) {
351
            $doc->setCreator($user);
352
        }
353
354
        $em = $this->em();
355
        $em->persist($doc);
356
        $em->flush();
357
358
        return $doc->getResourceNode();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $doc->getResourceNode() could return the type null which is incompatible with the type-hinted return Chamilo\CoreBundle\Entity\ResourceNode. Consider adding an additional type-check to rule them out.
Loading history...
359
    }
360
361
    public function findChildNodeByTitle(ResourceNode $parent, string $title): ?ResourceNode
362
    {
363
        return $this->em()
364
            ->getRepository(ResourceNode::class)
365
            ->findOneBy([
366
                'parent' => $parent->getId(),
367
                'title' => $title,
368
            ])
369
        ;
370
    }
371
372
    /**
373
     * Ensure "Learning paths" exists directly under the course resource node.
374
     * Links are created for course (and optional session) context.
375
     */
376
    public function ensureLearningPathSystemFolder(Course $course, ?Session $session = null): ResourceNode
377
    {
378
        $courseRoot = $course->getResourceNode();
379
        if (!$courseRoot instanceof ResourceNode) {
380
            throw new RuntimeException('Course has no ResourceNode root.');
381
        }
382
383
        // Try common i18n variants first
384
        $candidates = array_values(array_unique(array_filter([
385
            \function_exists('get_lang') ? get_lang('Learning paths') : null,
386
            \function_exists('get_lang') ? get_lang('Learning path') : null,
387
            'Learning paths',
388
            'Learning path',
389
        ])));
390
391
        foreach ($candidates as $title) {
392
            if ($child = $this->findChildNodeByTitle($courseRoot, $title)) {
393
                return $child;
394
            }
395
        }
396
397
        // Create "Learning paths" directly under the course root
398
        return $this->ensureFolder(
399
            $course,
400
            $courseRoot,
401
            'Learning paths',
402
            ResourceLink::VISIBILITY_DRAFT,
403
            $session
404
        );
405
    }
406
407
    /**
408
     * Recursively list all files (not folders) under a CDocument folder by its iid.
409
     * Returns items ready for exporters.
410
     */
411
    public function listFilesByParentIid(int $parentIid): array
412
    {
413
        $em = $this->getEntityManager();
414
415
        /** @var CDocument|null $parentDoc */
416
        $parentDoc = $this->findOneBy(['iid' => $parentIid]);
417
        if (!$parentDoc instanceof CDocument) {
418
            return [];
419
        }
420
421
        $parentNode = $parentDoc->getResourceNode();
422
        if (!$parentNode instanceof ResourceNode) {
423
            return [];
424
        }
425
426
        $out = [];
427
        $stack = [$parentNode->getId()];
428
429
        $projectDir   = Container::$container->get('kernel')->getProjectDir();
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

429
        $projectDir   = Container::$container->/** @scrutinizer ignore-call */ get('kernel')->getProjectDir();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
430
        $resourceBase = rtrim($projectDir, '/').'/var/upload/resource';
431
432
        /** @var ResourceNodeRepository $rnRepo */
433
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
434
435
        while ($stack) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stack of type array<integer,integer|null> 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...
436
            $pid = array_pop($stack);
437
438
            $qb = $em->createQueryBuilder()
439
                ->select('d', 'rn')
440
                ->from(CDocument::class, 'd')
441
                ->innerJoin('d.resourceNode', 'rn')
442
                ->andWhere('rn.parent = :pid')
443
                ->setParameter('pid', $pid);
444
445
            /** @var CDocument[] $children */
446
            $children = $qb->getQuery()->getResult();
447
448
            foreach ($children as $doc) {
449
                $filetype = (string) $doc->getFiletype();
450
                $rn = $doc->getResourceNode();
451
452
                if ($filetype === 'folder') {
453
                    if ($rn) {
454
                        $stack[] = $rn->getId();
455
                    }
456
                    continue;
457
                }
458
459
                if ($filetype === 'file') {
460
                    $fullPath = (string) $doc->getFullPath(); // e.g. "document/Folder/file.ext"
461
                    $relPath  = preg_replace('#^document/+#', '', $fullPath) ?? $fullPath;
462
463
                    $absPath = null;
464
                    $size    = 0;
465
466
                    if ($rn) {
467
                        $file = $rn->getFirstResourceFile(); /** @var ResourceFile|null $file */
468
                        if ($file) {
469
                            $storedRel = (string) $rnRepo->getFilename($file);
470
                            if ($storedRel !== '') {
471
                                $candidate = $resourceBase.$storedRel;
472
                                if (is_readable($candidate)) {
473
                                    $absPath = $candidate;
474
                                    $size    = (int) $file->getSize();
475
                                    if ($size <= 0 && is_file($candidate)) {
476
                                        $st   = @stat($candidate);
477
                                        $size = $st ? (int) $st['size'] : 0;
478
                                    }
479
                                }
480
                            }
481
                        }
482
                    }
483
484
                    $out[] = [
485
                        'id'       => (int) $doc->getIid(),
486
                        'path'     => $relPath,
487
                        'size'     => (int) $size,
488
                        'title'    => (string) $doc->getTitle(),
489
                        'abs_path' => $absPath,
490
                    ];
491
                }
492
            }
493
        }
494
495
        return $out;
496
    }
497
498
    /**
499
     * Return absolute filesystem path for a file CDocument if resolvable; null otherwise.
500
     */
501
    public function getAbsolutePathForDocument(CDocument $doc): ?string
502
    {
503
        $rn = $doc->getResourceNode();
504
        if (!$rn) {
505
            return null;
506
        }
507
508
        /** @var ResourceNodeRepository $rnRepo */
509
        $rnRepo = Container::$container->get(ResourceNodeRepository::class);
510
        $file   = $rn->getFirstResourceFile(); /** @var ResourceFile|null $file */
511
        if (!$file) {
512
            return null;
513
        }
514
515
        $storedRel = (string) $rnRepo->getFilename($file);
516
        if ($storedRel === '') {
517
            return null;
518
        }
519
520
        $projectDir   = Container::$container->get('kernel')->getProjectDir();
521
        $resourceBase = rtrim($projectDir, '/').'/var/upload/resource';
522
523
        $candidate = $resourceBase.$storedRel;
524
        return is_readable($candidate) ? $candidate : null;
525
    }
526
527
    private function safeTitle(string $name): string
528
    {
529
        $name = trim($name);
530
        $name = str_replace(['/', '\\'], '-', $name);
531
532
        return preg_replace('/\s+/', ' ', $name) ?: 'Untitled';
533
    }
534
535
    private function em(): EntityManagerInterface
536
    {
537
        /** @var EntityManagerInterface $em */
538
        return $this->getEntityManager();
539
    }
540
}
541