Passed
Pull Request — master (#6750)
by
unknown
08:11
created

ensureCourseDocumentsRootNode()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 20
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 33
rs 9.6
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
     *   /Learning paths/SCORM - {lp_id} - {lp_title}/{lp_title}.zip
80
     * Folder is teacher-only (DRAFT visibility by default).
81
     */
82
    public function registerScormZip(Course $course, CLp $lp, UploadedFile $zip): void
83
    {
84
        $em    = $this->em();
85
        $root  = $this->ensureCourseDocumentsRootNode($course);
86
87
        // Top folder for learning paths (use PUBLISHED if you want to see it without teacher role)
88
        $lpTop = $this->ensureFolder(
89
            $course,
90
            $root,
91
            'Learning paths',
92
            ResourceLink::VISIBILITY_DRAFT
93
        );
94
95
        // Subfolder per LP
96
        $lpFolderTitle = \sprintf('SCORM - %d - %s', $lp->getIid(), $this->safeTitle($lp->getTitle()));
97
        $lpFolder = $this->ensureFolder(
98
            $course,
99
            $lpTop,
100
            $lpFolderTitle,
101
            ResourceLink::VISIBILITY_DRAFT
102
        );
103
104
        // Store the ZIP as a "file" document under the LP folder
105
        $this->createFileInFolder(
106
            $course,
107
            $lpFolder,
108
            $zip,
109
            \sprintf('SCORM ZIP for LP #%d', $lp->getIid()),
110
            ResourceLink::VISIBILITY_DRAFT
111
        );
112
113
        $em->flush();
114
    }
115
116
    /**
117
     * Remove the LP subfolder "SCORM - {lp_id} - ..." under "Learning paths".
118
     */
119
    public function purgeScormZip(Course $course, CLp $lp): void
120
    {
121
        $em = $this->em();
122
        $prefix = \sprintf('SCORM - %d - ', $lp->getIid());
123
124
        // Newer layout: folders directly under the course root
125
        $courseRoot = $course->getResourceNode();
126
        if ($courseRoot) {
127
            // SCORM folder directly under course root
128
            if ($this->tryDeleteFirstFolderByTitlePrefix($courseRoot, $prefix)) {
129
                $em->flush();
130
                return;
131
            }
132
133
            // SCORM folder under "Learning paths" (which itself is under course root)
134
            $lpTop = $this->findChildNodeByTitle($courseRoot, 'Learning paths');
135
            if ($lpTop && $this->tryDeleteFirstFolderByTitlePrefix($lpTop, $prefix)) {
136
                $em->flush();
137
                return;
138
            }
139
        }
140
141
        // Legacy layout: under Documents → Learning paths
142
        $docsRoot = $this->getCourseDocumentsRootNode($course);
143
        if ($docsRoot) {
144
            $lpTop = $this->findChildNodeByTitle($docsRoot, 'Learning paths');
145
            if ($lpTop && $this->tryDeleteFirstFolderByTitlePrefix($lpTop, $prefix)) {
146
                $em->flush();
147
            }
148
        }
149
    }
150
151
    /**
152
     * Try to delete the first child folder whose title starts with $prefix under $parent.
153
     * Returns true if something was removed.
154
     */
155
    private function tryDeleteFirstFolderByTitlePrefix(ResourceNode $parent, string $prefix): bool
156
    {
157
        $em = $this->em();
158
        $qb = $em->createQueryBuilder()
159
            ->select('rn')
160
            ->from(ResourceNode::class, 'rn')
161
            ->where('rn.parent = :parent')
162
            ->andWhere('rn.title LIKE :prefix')
163
            ->setParameters(['parent' => $parent, 'prefix' => $prefix.'%'])
164
            ->setMaxResults(1);
165
166
        /** @var ResourceNode|null $node */
167
        $node = $qb->getQuery()->getOneOrNullResult();
168
        if ($node) {
169
            $em->remove($node);
170
            return true;
171
        }
172
173
        return false;
174
    }
175
176
    /**
177
     * Find the course Documents root node.
178
     *
179
     * Primary: parent = course.resourceNode
180
     * Fallback (legacy): parent IS NULL
181
     */
182
    public function getCourseDocumentsRootNode(Course $course): ?ResourceNode
183
    {
184
        $em  = $this->em();
185
        $rt  = $this->getResourceType();
186
        $courseNode = $course->getResourceNode();
187
188
        if ($courseNode) {
189
            $node = $em->createQuery(
190
                'SELECT rn
191
                   FROM Chamilo\CoreBundle\Entity\ResourceNode rn
192
                   JOIN rn.resourceType rt
193
                   JOIN rn.resourceLinks rl
194
                  WHERE rn.parent = :parent
195
                    AND rt = :rtype
196
                    AND rl.course = :course
197
               ORDER BY rn.id ASC'
198
            )
199
                ->setParameters([
200
                    'parent' => $courseNode,
201
                    'rtype'  => $rt,
202
                    'course' => $course,
203
                ])
204
                ->setMaxResults(1)
205
                ->getOneOrNullResult();
206
207
            if ($node) {
208
                return $node;
209
            }
210
        }
211
212
        // Fallback for historical data (Documents root directly under NULL)
213
        return $em->createQuery(
214
            'SELECT rn
215
               FROM Chamilo\CoreBundle\Entity\ResourceNode rn
216
               JOIN rn.resourceType rt
217
               JOIN rn.resourceLinks rl
218
              WHERE rn.parent IS NULL
219
                AND rt = :rtype
220
                AND rl.course = :course
221
           ORDER BY rn.id ASC'
222
        )
223
            ->setParameters(['rtype' => $rt, 'course' => $course])
224
            ->setMaxResults(1)
225
            ->getOneOrNullResult();
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
        /** @var User|null $user */
241
        $user = \api_get_user_entity();
242
243
        $node = new ResourceNode();
244
        $node->setTitle('Documents');
245
        $node->setResourceType($type);
246
        $node->setPublic(false);
247
248
        if ($course->getResourceNode()) {
249
            $node->setParent($course->getResourceNode());
250
        }
251
252
        if ($user) {
253
            $node->setCreator($user);
254
        }
255
256
        $link = new ResourceLink();
257
        $link->setCourse($course);
258
        $link->setVisibility(ResourceLink::VISIBILITY_PUBLISHED);
259
        $node->addResourceLink($link);
260
261
        $em->persist($node);
262
        $em->flush();
263
264
        return $node;
265
    }
266
267
    /**
268
     * Create (if missing) a folder under $parent using the API path:
269
     * create a CDocument(folder) + setParentResourceNode + setResourceLinkArray and let
270
     * the ResourceListener build the ResourceNode.
271
     */
272
    public function ensureFolder(
273
        Course $course,
274
        ResourceNode $parent,
275
        string $folderTitle,
276
        int $visibility = ResourceLink::VISIBILITY_DRAFT
277
    ): ResourceNode {
278
        if ($child = $this->findChildNodeByTitle($parent, $folderTitle)) {
279
            return $child;
280
        }
281
282
        /** @var User|null $user */
283
        $user = \api_get_user_entity();
284
285
        $doc = new CDocument();
286
        $doc->setTitle($folderTitle);
287
        $doc->setFiletype('folder');
288
        $doc->setParentResourceNode($course->getResourceNode()->getId());
289
        $doc->setResourceLinkArray([[
290
            'cid' => $course->getId(),
291
            'visibility' => $visibility,
292
        ]]);
293
        if ($user) {
294
            $doc->setCreator($user);
295
        }
296
297
        $em = $this->em();
298
        $em->persist($doc);
299
        $em->flush();
300
301
        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...
302
    }
303
304
    /**
305
     * Create a file under $parent using the API path:
306
     * CDocument(file) + setUploadFile + setParentResourceNode + setResourceLinkArray.
307
     */
308
    public function createFileInFolder(
309
        Course $course,
310
        ResourceNode $parent,
311
        UploadedFile $uploaded,
312
        string $comment,
313
        int $visibility
314
    ): ResourceNode {
315
        /** @var User|null $user */
316
        $user = \api_get_user_entity();
317
318
        $title = $uploaded->getClientOriginalName();
319
320
        $doc = new CDocument();
321
        $doc->setTitle($title);
322
        $doc->setFiletype('file');
323
        $doc->setComment($comment);
324
        $doc->setParentResourceNode($parent->getId());
325
        $doc->setResourceLinkArray([[
326
            'cid' => $course->getId(),
327
            'visibility' => $visibility,
328
        ]]);
329
        $doc->setUploadFile($uploaded);
330
331
        if ($user) {
332
            $doc->setCreator($user);
333
        }
334
335
        $em = $this->em();
336
        $em->persist($doc);
337
        $em->flush();
338
339
        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...
340
    }
341
342
    public function findChildNodeByTitle(ResourceNode $parent, string $title): ?ResourceNode
343
    {
344
        return $this->em()
345
            ->getRepository(ResourceNode::class)
346
            ->findOneBy(['parent' => $parent, 'title' => $title]);
347
    }
348
349
    private function safeTitle(string $name): string
350
    {
351
        $name = \trim($name);
352
        $name = \str_replace(['/', '\\'], '-', $name);
353
        return \preg_replace('/\s+/', ' ', $name) ?: 'Untitled';
354
    }
355
356
    private function em(): EntityManagerInterface
357
    {
358
        /** @var EntityManagerInterface $em */
359
        $em = $this->getEntityManager();
360
        return $em;
361
    }
362
363
}
364