Passed
Pull Request — master (#6257)
by
unknown
09:27
created

StudentPublicationController::cleanFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Entity\Session;
10
use Chamilo\CoreBundle\Entity\User;
11
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
12
use Chamilo\CoreBundle\ServiceHelper\CidReqHelper;
13
use Chamilo\CoreBundle\ServiceHelper\MessageHelper;
14
use Chamilo\CourseBundle\Entity\CStudentPublicationCorrection;
15
use Chamilo\CourseBundle\Repository\CStudentPublicationCorrectionRepository;
16
use Chamilo\CourseBundle\Repository\CStudentPublicationRepository;
17
use Doctrine\ORM\EntityManagerInterface;
18
use Mpdf\Mpdf;
19
use Mpdf\MpdfException;
20
use Mpdf\Output\Destination;
21
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
22
use Symfony\Bundle\SecurityBundle\Security;
23
use Symfony\Component\Finder\Finder;
24
use Symfony\Component\HttpFoundation\File\UploadedFile;
25
use Symfony\Component\HttpFoundation\JsonResponse;
26
use Symfony\Component\HttpFoundation\Request;
27
use Symfony\Component\HttpFoundation\Response;
28
use Symfony\Component\Routing\Annotation\Route;
29
use Symfony\Component\Serializer\SerializerInterface;
30
use Symfony\Contracts\Translation\TranslatorInterface;
31
32
#[Route('/assignments')]
33
class StudentPublicationController extends AbstractController
34
{
35
    public function __construct(
36
        private readonly CStudentPublicationRepository $studentPublicationRepo,
37
        private readonly CidReqHelper $cidReqHelper
38
    ) {}
39
40
41
    #[Route('/student', name: 'chamilo_core_assignment_student_list', methods: ['GET'])]
42
    public function getStudentAssignments(SerializerInterface $serializer): JsonResponse
43
    {
44
        $course = $this->cidReqHelper->getCourseEntity();
45
        $session = $this->cidReqHelper->getSessionEntity();
46
47
        $assignments = $this->studentPublicationRepo->findVisibleAssignmentsForStudent($course, $session);
48
49
        $data = array_map(function ($row) use ($serializer) {
50
            $publication = $row[0] ?? null;
51
            $commentsCount = (int) ($row['commentsCount'] ?? 0);
52
            $correctionsCount = (int) ($row['correctionsCount'] ?? 0);
53
            $lastUpload = $row['lastUpload'] ?? null;
54
55
            $item = json_decode($serializer->serialize($publication, 'json', [
56
                'groups' => ['student_publication:read'],
57
            ]), true);
58
59
            $item['commentsCount'] = $commentsCount;
60
            $item['feedbackCount'] = $correctionsCount;
61
            $item['lastUpload'] = $lastUpload;
62
63
            return $item;
64
        }, $assignments);
65
66
        return new JsonResponse([
67
            'hydra:member' => $data,
68
            'hydra:totalItems' => count($data),
69
        ]);
70
    }
71
72
    #[Route('/progress', name: 'chamilo_core_assignment_student_progress', methods: ['GET'])]
73
    public function getStudentProgress(
74
        SerializerInterface $serializer
75
    ): JsonResponse {
76
        $course = $this->cidReqHelper->getCourseEntity();
77
        $session = $this->cidReqHelper->getSessionEntity();
78
79
        $progressList = $this->studentPublicationRepo->findStudentProgressByCourse($course, $session);
80
81
        return new JsonResponse([
82
            'hydra:member' => $progressList,
83
            'hydra:totalItems' => count($progressList),
84
        ]);
85
    }
86
87
    #[Route('/{assignmentId}/submissions', name: 'chamilo_core_assignment_student_submission_list', methods: ['GET'])]
88
    public function getAssignmentSubmissions(
89
        int $assignmentId,
90
        Request $request,
91
        SerializerInterface $serializer,
92
        CStudentPublicationRepository $repo,
93
        Security $security
94
    ): JsonResponse {
95
        /* @var User $user */
96
        $user = $security->getUser();
97
98
        $page = (int) $request->query->get('page', 1);
99
        $itemsPerPage = (int) $request->query->get('itemsPerPage', 10);
100
        $order = $request->query->all('order');
101
102
        [$submissions, $total] = $repo->findAssignmentSubmissionsPaginated(
103
            $assignmentId,
104
            $user,
105
            $page,
106
            $itemsPerPage,
107
            $order
108
        );
109
110
        $data = json_decode($serializer->serialize(
111
            $submissions,
112
            'json',
113
            ['groups' => ['student_publication:read']]
114
        ), true);
115
116
        return new JsonResponse([
117
            'hydra:member' => $data,
118
            'hydra:totalItems' => $total,
119
        ]);
120
    }
121
122
    #[Route('/{assignmentId}/submissions/teacher', name: 'chamilo_core_assignment_teacher_submission_list', methods: ['GET'])]
123
    public function getAssignmentSubmissionsForTeacher(
124
        int $assignmentId,
125
        Request $request,
126
        SerializerInterface $serializer,
127
        CStudentPublicationRepository $repo
128
    ): JsonResponse {
129
        $page = (int) $request->query->get('page', 1);
130
        $itemsPerPage = (int) $request->query->get('itemsPerPage', 10);
131
        $order = $request->query->all('order');
132
133
        [$submissions, $total] = $repo->findAllSubmissionsByAssignment(
134
            $assignmentId,
135
            $page,
136
            $itemsPerPage,
137
            $order
138
        );
139
140
        $data = json_decode($serializer->serialize(
141
            $submissions,
142
            'json',
143
            ['groups' => ['student_publication:read', 'resource_node:read']]
144
        ), true);
145
146
        return new JsonResponse([
147
            'hydra:member' => $data,
148
            'hydra:totalItems' => $total,
149
        ]);
150
    }
151
152
    #[Route('/submissions/{id}', name: 'chamilo_core_assignment_student_submission_delete', methods: ['DELETE'])]
153
    public function deleteSubmission(
154
        int $id,
155
        EntityManagerInterface $em,
156
        CStudentPublicationRepository $repo
157
    ): JsonResponse {
158
        $submission = $repo->find($id);
159
160
        if (!$submission) {
161
            return new JsonResponse(['error' => 'Submission not found.'], 404);
162
        }
163
164
        $this->denyAccessUnlessGranted('DELETE', $submission->getResourceNode());
165
166
        $em->remove($submission->getResourceNode());
167
        $em->flush();
168
169
        return new JsonResponse(null, 204);
170
    }
171
172
    #[Route('/submissions/{id}/edit', name: 'chamilo_core_assignment_student_submission_edit', methods: ['PATCH'])]
173
    public function editSubmission(
174
        int $id,
175
        Request $request,
176
        EntityManagerInterface $em,
177
        CStudentPublicationRepository $repo,
178
        MessageHelper $messageHelper
179
    ): JsonResponse {
180
        $submission = $repo->find($id);
181
182
        if (!$submission) {
183
            return new JsonResponse(['error' => 'Submission not found.'], 404);
184
        }
185
186
        $this->denyAccessUnlessGranted('EDIT', $submission->getResourceNode());
187
188
        $data = json_decode($request->getContent(), true);
189
        $title = $data['title'] ?? null;
190
        $description = $data['description'] ?? null;
191
        $sendMail = $data['sendMail'] ?? false;
192
193
        if ($title !== null) {
194
            $submission->setTitle($title);
195
        }
196
        if ($description !== null) {
197
            $submission->setDescription($description);
198
        }
199
200
        $em->flush();
201
202
        if ($sendMail) {
203
            $user = $submission->getUser();
204
            if ($user) {
205
                $messageSubject = sprintf('Feedback updated for "%s"', $submission->getTitle());
206
                $messageContent = sprintf(
207
                    'There is a new feedback update for your submission "%s". Please check it on the platform.',
208
                    $submission->getTitle()
209
                );
210
211
                $messageHelper->sendMessageSimple(
212
                    $user->getId(),
213
                    $messageSubject,
214
                    $messageContent,
215
                    $this->getUser()->getId()
216
                );
217
            }
218
        }
219
220
        return new JsonResponse(['success' => true]);
221
    }
222
223
    #[Route('/submissions/{id}/move', name: 'chamilo_core_assignment_student_submission_move', methods: ['PATCH'])]
224
    public function moveSubmission(
225
        int $id,
226
        Request $request,
227
        EntityManagerInterface $em,
228
        CStudentPublicationRepository $repo
229
    ): JsonResponse {
230
        $submission = $repo->find($id);
231
232
        if (!$submission) {
233
            return new JsonResponse(['error' => 'Submission not found.'], 404);
234
        }
235
236
        $this->denyAccessUnlessGranted('EDIT', $submission->getResourceNode());
237
238
        $data = json_decode($request->getContent(), true);
239
        $newAssignmentId = $data['newAssignmentId'] ?? null;
240
241
        if (!$newAssignmentId) {
242
            return new JsonResponse(['error' => 'New assignment ID is required.'], 400);
243
        }
244
245
        $newParent = $repo->find($newAssignmentId);
246
247
        if (!$newParent) {
248
            return new JsonResponse(['error' => 'Target assignment not found.'], 404);
249
        }
250
251
        $submission->setPublicationParent($newParent);
252
253
        $em->flush();
254
255
        return new JsonResponse(['success' => true]);
256
    }
257
258
    #[Route('/{assignmentId}/unsubmitted-users', name: 'chamilo_core_assignment_unsubmitted_users', methods: ['GET'])]
259
    public function getUnsubmittedUsers(
260
        int $assignmentId,
261
        SerializerInterface $serializer,
262
        CStudentPublicationRepository $repo
263
    ): JsonResponse {
264
        $course = $this->cidReqHelper->getCourseEntity();
265
        $session = $this->cidReqHelper->getSessionEntity();
266
267
        $students = $session
268
            ? $session->getSessionRelCourseRelUsersByStatus($course, Session::STUDENT)
269
            : $course->getStudentSubscriptions();
270
271
        $studentIds = array_map(fn ($rel) => $rel->getUser()->getId(), $students->toArray());
272
273
        $submittedUserIds = $repo->findUserIdsWithSubmissions($assignmentId);
274
275
        $unsubmitted = array_filter(
276
            $students->toArray(),
277
            fn ($rel) => !in_array($rel->getUser()->getId(), $submittedUserIds, true)
278
        );
279
280
        $data = array_values(array_map(fn ($rel) => $rel->getUser(), $unsubmitted));
281
282
        return $this->json([
283
            'hydra:member' => $data,
284
            'hydra:totalItems' => count($data),
285
        ], 200, [], ['groups' => ['user:read']]);
286
    }
287
288
    #[Route('/{assignmentId}/unsubmitted-users/email', name: 'chamilo_core_assignment_unsubmitted_users_email', methods: ['POST'])]
289
    public function emailUnsubmittedUsers(
290
        int $assignmentId,
291
        CStudentPublicationRepository $repo,
292
        MessageHelper $messageHelper,
293
        Security $security
294
    ): JsonResponse {
295
        $course = $this->cidReqHelper->getCourseEntity();
296
        $session = $this->cidReqHelper->getSessionEntity();
297
298
        /* @var User $user */
299
        $user = $security->getUser();
300
        $senderId = $user?->getId();
301
302
        $students = $session
303
            ? $session->getSessionRelCourseRelUsersByStatus($course, Session::STUDENT)
304
            : $course->getStudentSubscriptions();
305
306
        $submittedUserIds = $repo->findUserIdsWithSubmissions($assignmentId);
307
308
        $unsubmitted = array_filter(
309
            $students->toArray(),
310
            fn ($rel) => !in_array($rel->getUser()->getId(), $submittedUserIds, true)
311
        );
312
313
        foreach ($unsubmitted as $rel) {
314
            $user = $rel->getUser();
315
            $messageHelper->sendMessageSimple(
316
                $user->getId(),
317
                "You have not submitted your assignment",
318
                "Please submit your assignment as soon as possible.",
319
                $senderId
320
            );
321
        }
322
323
        return new JsonResponse(['success' => true, 'sent' => count($unsubmitted)]);
324
    }
325
326
    #[Route('/{id}/export/pdf', name: 'chamilo_core_assignment_export_pdf', methods: ['GET'])]
327
    public function exportPdf(
328
        int $id,
329
        Request $request,
330
        CStudentPublicationRepository $repo
331
    ): Response {
332
        $course = $this->cidReqHelper->getCourseEntity();
333
        $session = $this->cidReqHelper->getSessionEntity();
334
335
        $assignment = $repo->find($id);
336
337
        if (!$assignment) {
338
            throw $this->createNotFoundException('Assignment not found');
339
        }
340
341
        [$submissions] = $repo->findAllSubmissionsByAssignment(
342
            assignmentId: $assignment->getIid(),
343
            page: 1,
344
            itemsPerPage: 10000
345
        );
346
347
        $html = $this->renderView('@ChamiloCore/Work/pdf_export.html.twig', [
348
            'assignment' => $assignment,
349
            'course' => $course,
350
            'session' => $session,
351
            'submissions' => $submissions,
352
        ]);
353
354
        try {
355
            $mpdf = new Mpdf([
356
                'tempDir' => api_get_path(SYS_ARCHIVE_PATH) . 'mpdf/',
357
            ]);
358
            $mpdf->WriteHTML($html);
359
360
            return new Response(
361
                $mpdf->Output('', Destination::INLINE),
362
                200,
363
                ['Content-Type' => 'application/pdf']
364
            );
365
        } catch (MpdfException $e) {
366
            throw new \RuntimeException('Failed to generate PDF: '.$e->getMessage(), 500, $e);
367
        }
368
    }
369
370
    #[Route('/{assignmentId}/corrections/delete', name: 'chamilo_core_assignment_delete_all_corrections', methods: ['DELETE'])]
371
    public function deleteAllCorrections(
372
        int $assignmentId,
373
        EntityManagerInterface $em,
374
        CStudentPublicationRepository $repo
375
    ): JsonResponse {
376
        $submissions = $repo->findAllSubmissionsByAssignment($assignmentId, 1, 10000)[0];
377
378
        $count = 0;
379
380
        foreach ($submissions as $submission) {
381
            $correctionNode = $submission->getCorrection();
382
383
            if ($correctionNode !== null) {
384
                $em->remove($correctionNode);
385
                $submission->setExtensions(null);
386
                $count++;
387
            }
388
        }
389
390
        $em->flush();
391
392
        return new JsonResponse([
393
            'success' => true,
394
            'deleted' => $count,
395
        ]);
396
    }
397
398
    #[Route('/{assignmentId}/download-package', name: 'chamilo_core_assignment_download_package', methods: ['GET'])]
399
    public function downloadAssignmentPackage(
400
        int $assignmentId,
401
        CStudentPublicationRepository $repo,
402
        ResourceNodeRepository $resourceNodeRepository
403
    ): Response {
404
        $assignment = $repo->find($assignmentId);
405
406
        if (!$assignment) {
407
            throw $this->createNotFoundException('Assignment not found');
408
        }
409
410
        [$submissions] = $repo->findAllSubmissionsByAssignment($assignmentId, 1, 10000);
411
        $zipPath = api_get_path(SYS_ARCHIVE_PATH) . uniqid('assignment_', true) . '.zip';
412
        $zip = new \ZipArchive();
413
414
        if ($zip->open($zipPath, \ZipArchive::CREATE) !== true) {
415
            throw new \RuntimeException('Cannot create zip archive');
416
        }
417
418
        foreach ($submissions as $submission) {
419
            $resourceNode = $submission->getResourceNode();
420
            $resourceFile = $resourceNode?->getFirstResourceFile();
421
            $user = $submission->getUser();
422
            $sentDate = $submission->getSentDate()?->format('Y-m-d_H-i') ?? 'unknown';
423
424
            if ($resourceFile) {
425
                try {
426
                    $path = $resourceNodeRepository->getFilename($resourceFile);
427
                    $content = $resourceNodeRepository->getFileSystem()->read($path);
428
429
                    $filename = sprintf('%s_%s_%s', $sentDate, $user->getUsername(), $resourceFile->getOriginalName());
430
                    $zip->addFromString($filename, $content);
431
                } catch (\Throwable $e) {
432
                    continue;
433
                }
434
            }
435
        }
436
437
        $zip->close();
438
439
        return $this->file($zipPath, $assignment->getTitle() . '.zip')->deleteFileAfterSend();
440
    }
441
442
    #[Route('/{assignmentId}/upload-corrections-package', name: 'chamilo_core_assignment_upload_corrections_package', methods: ['POST'])]
443
    public function uploadCorrectionsPackage(
444
        int $assignmentId,
445
        Request $request,
446
        CStudentPublicationRepository $repo,
447
        CStudentPublicationCorrectionRepository $correctionRepo,
448
        EntityManagerInterface $em,
449
        TranslatorInterface $translator
450
    ): JsonResponse {
451
        $file = $request->files->get('file');
452
        if (!$file || $file->getClientOriginalExtension() !== 'zip') {
453
            return new JsonResponse(['error' => 'Invalid file'], 400);
454
        }
455
456
        $folder = uniqid('corrections_', true);
457
        $destinationDir = api_get_path(SYS_ARCHIVE_PATH) . $folder;
458
        mkdir($destinationDir, 0777, true);
459
460
        $zip = new \ZipArchive();
461
        $zip->open($file->getPathname());
462
        $zip->extractTo($destinationDir);
463
        $zip->close();
464
465
        [$submissions] = $repo->findAllSubmissionsByAssignment($assignmentId, 1, 10000);
466
467
        $matchMap = [];
468
        foreach ($submissions as $submission) {
469
            $date = $submission->getSentDate()?->format('Y-m-d_H-i') ?? 'unknown';
470
            $username = $submission->getUser()?->getUsername() ?? 'unknown';
471
            $title = $this->cleanFilename($submission->getTitle() ?? '');
472
            $title = preg_replace('/_[a-f0-9]{10,}$/', '', pathinfo($title, PATHINFO_FILENAME));
473
            $key = sprintf('%s_%s_%s', $date, $username, $title);
474
            $matchMap[$key] = $submission;
475
        }
476
477
        $finder = new Finder();
478
        $finder->files()->in($destinationDir);
479
480
        $uploaded = 0;
481
        $skipped = [];
482
483
        foreach ($finder as $foundFile) {
484
            $filename = $foundFile->getFilename();
485
            $nameOnly = pathinfo($filename, PATHINFO_FILENAME);
486
            $nameOnly = preg_replace('/_[a-f0-9]{10,}$/', '', $nameOnly);
487
488
            $matched = false;
489
            foreach ($matchMap as $prefix => $submission) {
490
                if ($nameOnly === $prefix) {
491
492
                    if ($submission->getCorrection()) {
493
                        $em->remove($submission->getCorrection());
494
                        $em->flush();
495
                    }
496
497
                    $uploadedFile = new UploadedFile(
498
                        $foundFile->getRealPath(),
499
                        $filename,
500
                        null,
501
                        null,
502
                        true
503
                    );
504
505
                    $correction = new CStudentPublicationCorrection();
506
                    $correction->setTitle($filename);
507
                    $correction->setUploadFile($uploadedFile);
508
                    $correction->setParentResourceNode($submission->getResourceNode()->getId());
509
510
                    $em->persist($correction);
511
512
                    $submission->setExtensions($filename);
513
                    $submission->setDescription('Correction uploaded');
514
                    $submission->setQualification(0);
515
                    $submission->setDateOfQualification(new \DateTime());
516
                    $submission->setAccepted(true);
517
                    $em->persist($submission);
518
519
                    $uploaded++;
520
                    $matched = true;
521
                    break;
522
                }
523
            }
524
525
            if (!$matched) {
526
                $skipped[] = $filename;
527
            }
528
        }
529
530
        $em->flush();
531
532
        return new JsonResponse([
533
            'success' => true,
534
            'uploaded' => $uploaded,
535
            'skipped' => count($skipped),
536
            'skipped_files' => $skipped,
537
        ]);
538
    }
539
540
    /**
541
     * Sanitize filenames.
542
     */
543
    private function cleanFilename(string $name): string
544
    {
545
        $name = str_replace([':', '\\', '/', '*', '?', '"', '<', '>', '|'], '_', $name);
546
        $name = preg_replace('/\s+/', '_', $name);
547
        $name = preg_replace('/[^\w\-\.]/u', '', $name);
548
        $name = preg_replace('/_+/', '_', $name);
549
        return trim($name, '_');
550
    }
551
}
552