Passed
Push — master ( fc7894...4ec0d9 )
by Yannick
08:29
created

CDocumentRepository::purgeScormZip()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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