Passed
Push — master ( e48f96...18d135 )
by Angel Fernando Quiroz
19:42 queued 01:03
created

BaseResourceFileAction::buildFolderStructure()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 2
dl 0
loc 20
rs 9.9332
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\Api;
8
9
use Chamilo\CoreBundle\Component\Utils\CreateUploadedFile;
10
use Chamilo\CoreBundle\Entity\AbstractResource;
11
use Chamilo\CoreBundle\Entity\Course;
12
use Chamilo\CoreBundle\Entity\ResourceLink;
13
use Chamilo\CoreBundle\Entity\ResourceRight;
14
use Chamilo\CoreBundle\Entity\Session;
15
use Chamilo\CoreBundle\Entity\User;
16
use Chamilo\CoreBundle\Repository\ResourceRepository;
17
use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceNodeVoter;
18
use Chamilo\CourseBundle\Entity\CDocument;
19
use Chamilo\CourseBundle\Entity\CGroup;
20
use DateTime;
21
use Doctrine\ORM\EntityManager;
22
use Doctrine\ORM\EntityManagerInterface;
23
use Exception;
24
use InvalidArgumentException;
25
use Symfony\Component\HttpFoundation\File\UploadedFile;
26
use Symfony\Component\HttpFoundation\Request;
27
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
28
use Symfony\Component\HttpKernel\KernelInterface;
29
30
class BaseResourceFileAction
31
{
32
    public static function setLinks(AbstractResource $resource, EntityManagerInterface $em): void
33
    {
34
        $resourceNode = $resource->getResourceNode();
35
        $links = $resource->getResourceLinkArray();
36
        if ($links) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $links 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...
37
            $groupRepo = $em->getRepository(CGroup::class);
38
            $courseRepo = $em->getRepository(Course::class);
39
            $sessionRepo = $em->getRepository(Session::class);
40
            $userRepo = $em->getRepository(User::class);
41
42
            foreach ($links as $link) {
43
                $resourceLink = new ResourceLink();
44
                $linkSet = false;
45
                if (isset($link['cid']) && !empty($link['cid'])) {
46
                    $course = $courseRepo->find($link['cid']);
47
                    if (null !== $course) {
48
                        $linkSet = true;
49
                        $resourceLink->setCourse($course);
50
                    } else {
51
                        throw new InvalidArgumentException(sprintf('Course #%s does not exists', $link['cid']));
52
                    }
53
                }
54
55
                if (isset($link['sid']) && !empty($link['sid'])) {
56
                    $session = $sessionRepo->find($link['sid']);
57
                    if (null !== $session) {
58
                        $linkSet = true;
59
                        $resourceLink->setSession($session);
60
                    } else {
61
                        throw new InvalidArgumentException(sprintf('Session #%s does not exists', $link['sid']));
62
                    }
63
                }
64
65
                if (isset($link['gid']) && !empty($link['gid'])) {
66
                    $group = $groupRepo->find($link['gid']);
67
                    if (null !== $group) {
68
                        $linkSet = true;
69
                        $resourceLink->setGroup($group);
70
                    } else {
71
                        throw new InvalidArgumentException(sprintf('Group #%s does not exists', $link['gid']));
72
                    }
73
                }
74
75
                if (isset($link['uid']) && !empty($link['uid'])) {
76
                    $user = $userRepo->find($link['uid']);
77
                    if (null !== $user) {
78
                        $linkSet = true;
79
                        $resourceLink->setUser($user);
80
                    } else {
81
                        throw new InvalidArgumentException(sprintf('User #%s does not exists', $link['uid']));
82
                    }
83
                }
84
85
                if (isset($link['visibility'])) {
86
                    $resourceLink->setVisibility((int) $link['visibility']);
87
                } else {
88
                    throw new InvalidArgumentException('Link needs a visibility key');
89
                }
90
91
                if ($linkSet) {
92
                    $em->persist($resourceLink);
93
                    $resourceNode->addResourceLink($resourceLink);
94
                    // $em->persist($resourceNode);
95
                    // $em->persist($resource->getResourceNode());
96
                }
97
            }
98
        }
99
100
        // Use by Chamilo not api platform.
101
        $links = $resource->getResourceLinkEntityList();
102
        if ($links) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $links 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...
103
            // error_log('$resource->getResourceLinkEntityList()');
104
            foreach ($links as $link) {
105
                /*$rights = [];
106
                switch ($link->getVisibility()) {
107
                    case ResourceLink::VISIBILITY_PENDING:
108
                    case ResourceLink::VISIBILITY_DRAFT:
109
                        $editorMask = ResourceNodeVoter::getEditorMask();
110
                        $resourceRight = new ResourceRight();
111
                        $resourceRight
112
                            ->setMask($editorMask)
113
                            ->setRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_TEACHER)
114
                        ;
115
                        $rights[] = $resourceRight;
116
117
                        break;
118
                }
119
120
                if (!empty($rights)) {
121
                    foreach ($rights as $right) {
122
                        $link->addResourceRight($right);
123
                    }
124
                }*/
125
                // error_log('link adding to node: '.$resource->getResourceNode()->getId());
126
                // error_log('link with user : '.$link->getUser()->getUsername());
127
                $resource->getResourceNode()->addResourceLink($link);
128
129
                $em->persist($link);
130
            }
131
        }
132
    }
133
134
    /**
135
     * @todo use this function inside handleCreateFileRequest
136
     */
137
    protected function handleCreateRequest(AbstractResource $resource, ResourceRepository $resourceRepository, Request $request): array
138
    {
139
        $contentData = $request->getContent();
140
141
        if (!empty($contentData)) {
142
            $contentData = json_decode($contentData, true);
143
            $title = $contentData['title'] ?? '';
144
            $parentResourceNodeId = (int) ($contentData['parentResourceNodeId'] ?? 0);
145
            $resourceLinkList = $contentData['resourceLinkList'] ?? [];
146
            if (empty($resourceLinkList)) {
147
                $resourceLinkList = $contentData['resourceLinkListFromEntity'] ?? [];
148
            }
149
        } else {
150
            $contentData = $request->request->all();
151
            $title = $request->get('title');
152
            $parentResourceNodeId = (int) $request->get('parentResourceNodeId');
153
            $resourceLinkList = $request->get('resourceLinkList', []);
154
            if (!empty($resourceLinkList)) {
155
                $resourceLinkList = !str_contains($resourceLinkList, '[') ? json_decode('['.$resourceLinkList.']', true) : json_decode($resourceLinkList, true);
156
                if (empty($resourceLinkList)) {
157
                    $message = 'resourceLinkList is not a valid json. Use for example: [{"cid":1, "visibility":1}]';
158
159
                    throw new InvalidArgumentException($message);
160
                }
161
            }
162
        }
163
164
        if (0 === $parentResourceNodeId) {
165
            throw new Exception('Parameter parentResourceNodeId int value is needed');
166
        }
167
168
        $resource->setParentResourceNode($parentResourceNodeId);
169
170
        if (empty($title)) {
171
            throw new InvalidArgumentException('title is required');
172
        }
173
174
        $resource->setResourceName($title);
175
176
        // Set resource link list if exists.
177
        if (!empty($resourceLinkList)) {
178
            $resource->setResourceLinkArray($resourceLinkList);
179
        }
180
181
        return $contentData;
182
    }
183
184
    /**
185
     * Function loaded when creating a resource using the api, then the ResourceListener is executed.
186
     */
187
    protected function handleCreateFileRequest(AbstractResource $resource, ResourceRepository $resourceRepository, Request $request, EntityManager $em, String $fileExistsOption = ''): array
188
    {
189
        $contentData = $request->getContent();
190
191
        if (!empty($contentData)) {
192
            $contentData = json_decode($contentData, true);
193
            $title = $contentData['title'] ?? '';
194
            $comment = $contentData['comment'] ?? '';
195
            $parentResourceNodeId = (int) ($contentData['parentResourceNodeId'] ?? 0);
196
            $fileType = $contentData['filetype'] ?? '';
197
            $resourceLinkList = $contentData['resourceLinkList'] ?? [];
198
        } else {
199
            $title = $request->get('title');
200
            $comment = $request->get('comment');
201
            $parentResourceNodeId = (int) $request->get('parentResourceNodeId');
202
            $fileType = $request->get('filetype');
203
            $resourceLinkList = $request->get('resourceLinkList', []);
204
            if (!empty($resourceLinkList)) {
205
                $resourceLinkList = !str_contains($resourceLinkList, '[') ? json_decode('['.$resourceLinkList.']', true) : json_decode($resourceLinkList, true);
206
                if (empty($resourceLinkList)) {
207
                    $message = 'resourceLinkList is not a valid json. Use for example: [{"cid":1, "visibility":1}]';
208
209
                    throw new InvalidArgumentException($message);
210
                }
211
            }
212
        }
213
214
        if (empty($fileType)) {
215
            throw new Exception('filetype needed: folder or file');
216
        }
217
218
        if (0 === $parentResourceNodeId) {
219
            throw new Exception('parentResourceNodeId int value needed');
220
        }
221
222
        $resource->setParentResourceNode($parentResourceNodeId);
223
224
        switch ($fileType) {
225
            case 'certificate':
226
            case 'file':
227
                $content = '';
228
                if ($request->request->has('contentFile')) {
229
                    $content = $request->request->get('contentFile');
230
                }
231
                $fileParsed = false;
232
                // File upload.
233
                if ($request->files->count() > 0) {
234
                    if (!$request->files->has('uploadFile')) {
235
                        throw new BadRequestHttpException('"uploadFile" is required');
236
                    }
237
238
                    /** @var UploadedFile $uploadedFile */
239
                    $uploadedFile = $request->files->get('uploadFile');
240
                    $title = $uploadedFile->getClientOriginalName();
241
242
                    if (empty($title)) {
243
                        throw new InvalidArgumentException('title is required');
244
                    }
245
246
                    // Handle the appropriate action based on the fileExistsOption
247
                    if (!empty($fileExistsOption)) {
248
                        // Check if a document with the same title and parent resource node already exists
249
                        $existingDocument = $resourceRepository->findByTitleAndParentResourceNode($title, $parentResourceNodeId);
250
                        if ($existingDocument) {
251
                            switch ($fileExistsOption) {
252
                                case 'overwrite':
253
                                    // Perform actions when file exists and 'overwrite' option is selected
254
                                    $resource->setResourceName($title);
255
                                    $existingDocument->setTitle($title);
256
                                    $existingDocument->setComment($comment);
257
                                    $em->persist($existingDocument);
258
                                    $em->flush();
259
260
                                    // Return any data you need for further processing
261
                                    return [
262
                                        'filetype' => 'file',
263
                                        'comment' => $comment,
264
                                    ];
265
                                case 'rename':
266
                                    // Perform actions when file exists and 'rename' option is selected
267
                                    $newTitle = $this->generateUniqueTitle($title); // Generate a unique title
268
                                    $resource->setResourceName($newTitle);
269
                                    $resource->setUploadFile($uploadedFile);
270
                                    if (!empty($resourceLinkList)) {
271
                                        $resource->setResourceLinkArray($resourceLinkList);
272
                                    }
273
                                    $em->persist($resource);
274
                                    $em->flush();
275
276
                                    // Return any data you need for further processing
277
                                    return [
278
                                        'filetype' => 'file',
279
                                        'comment' => $comment,
280
                                    ];
281
                                case 'nothing':
282
                                    // Perform actions when file exists and 'nothing' option is selected
283
                                    // Display a message indicating that the file already exists
284
                                    // or perform any other desired actions based on your application's requirements
285
                                    $resource->setResourceName($title);
286
                                    $flashBag = $request->getSession()->getFlashBag();
287
                                    $flashBag->add('warning', 'Upload Already Exists');
288
                                    return [
289
                                        'filetype' => 'file',
290
                                        'comment' => $comment,
291
                                    ];
292
                                default:
293
                                    throw new InvalidArgumentException('Invalid fileExistsOption');
294
                            }
295
                        } else {
296
                            $resource->setResourceName($title);
297
                            $resource->setUploadFile($uploadedFile);
298
                            $fileParsed = true;
299
                        }
300
                    }
301
                }
302
303
                // Get data in content and create a HTML file.
304
                if (!$fileParsed && $content) {
305
                    $uploadedFile = CreateUploadedFile::fromString($title.'.html', 'text/html', $content);
306
                    $resource->setUploadFile($uploadedFile);
307
                    $fileParsed = true;
308
                }
309
310
                if (!$fileParsed) {
311
                    throw new InvalidArgumentException('filetype was set to "file" but not upload file found');
312
                }
313
314
                break;
315
316
            case 'folder':
317
                break;
318
        }
319
320
        // Set resource link list if exists.
321
        if (!empty($resourceLinkList)) {
322
            $resource->setResourceLinkArray($resourceLinkList);
323
        }
324
325
        return [
326
            'filetype' => $fileType,
327
            'comment' => $comment,
328
        ];
329
    }
330
331
    protected function handleCreateFileRequestUncompress(AbstractResource $resource, Request $request, EntityManager $em, KernelInterface $kernel): array
332
    {
333
        // Get the parameters from the request
334
        $parentResourceNodeId = (int) $request->get('parentResourceNodeId');
335
        $fileType = $request->get('filetype');
336
        $resourceLinkList = $request->get('resourceLinkList', []);
337
        if (!empty($resourceLinkList)) {
338
            $resourceLinkList = false === strpos($resourceLinkList, '[') ? json_decode('['.$resourceLinkList.']', true) : json_decode($resourceLinkList, true);
339
            if (empty($resourceLinkList)) {
340
                $message = 'resourceLinkList is not a valid json. Use for example: [{"cid":1, "visibility":1}]';
341
342
                throw new InvalidArgumentException($message);
343
            }
344
        }
345
346
        if (empty($fileType)) {
347
            throw new Exception('filetype needed: folder or file');
348
        }
349
350
        if (0 === $parentResourceNodeId) {
351
            throw new Exception('parentResourceNodeId int value needed');
352
        }
353
354
        if ('file' == $fileType && $request->files->count() > 0) {
355
            if (!$request->files->has('uploadFile')) {
356
                throw new BadRequestHttpException('"uploadFile" is required');
357
            }
358
359
            $uploadedFile = $request->files->get('uploadFile');
360
            $resourceTitle = $uploadedFile->getClientOriginalName();
361
            $resource->setResourceName($resourceTitle);
362
            $resource->setUploadFile($uploadedFile);
363
364
            if ('zip' === $uploadedFile->getClientOriginalExtension()) {
365
                // Extract the files and subdirectories
366
                $extractedData = $this->extractZipFile($uploadedFile, $kernel);
367
                $folderStructure = $extractedData['folderStructure'];
368
                $extractPath = $extractedData['extractPath'];
369
                $documents = $this->saveZipContentsAsDocuments($folderStructure, $em, $resourceLinkList, $parentResourceNodeId, '', $extractPath, $processedItems);
370
            }
371
        }
372
373
        $resource->setParentResourceNode($parentResourceNodeId);
374
375
        return [
376
            'filetype' => $fileType,
377
            'comment' => 'Uncompressed',
378
        ];
379
    }
380
381
    protected function handleUpdateRequest(AbstractResource $resource, ResourceRepository $repo, Request $request, EntityManager $em): AbstractResource
382
    {
383
        $contentData = $request->getContent();
384
        $resourceLinkList = [];
385
        if (!empty($contentData)) {
386
            $contentData = json_decode($contentData, true);
387
            $title = $contentData['title'] ?? '';
388
            $content = $contentData['contentFile'] ?? '';
389
            $resourceLinkList = $contentData['resourceLinkListFromEntity'] ?? [];
390
        } else {
391
            $title = $request->get('title');
392
            $content = $request->request->get('contentFile');
393
            // $comment = $request->request->get('comment');
394
        }
395
396
        $repo->setResourceName($resource, $title);
397
398
        $hasFile = $resource->getResourceNode()->hasResourceFile();
399
400
        $resourceNode = $resource->getResourceNode();
401
402
        if ($hasFile && !empty($content)) {
403
            if ($resourceNode->hasResourceFile()) {
404
                // The content is updated by the ResourceNodeListener.php
405
                $resourceNode->setContent($content);
406
                $resourceNode->getResourceFile()->setSize(\strlen($content));
407
            }
408
            $resourceNode->getResourceFile()->setUpdatedAt(new DateTime());
409
            $resource->setResourceNode($resourceNode);
410
        }
411
412
        $link = null;
413
        if (!empty($resourceLinkList)) {
414
            foreach ($resourceLinkList as $key => &$linkArray) {
415
                // Find the exact link.
416
                $linkId = $linkArray['id'] ?? 0;
417
                if (!empty($linkId)) {
418
                    /** @var ResourceLink $link */
419
                    $link = $resourceNode->getResourceLinks()->filter(fn ($link) => $link->getId() === $linkId)->first();
420
421
                    if (null !== $link) {
422
                        $link->setVisibility((int) $linkArray['visibility']);
423
                        unset($resourceLinkList[$key]);
424
425
                        $em->persist($link);
426
                    }
427
                }
428
            }
429
430
            $resource->setResourceLinkArray($resourceLinkList);
431
            self::setLinks($resource, $em);
432
        }
433
434
        $isRecursive = !$hasFile;
435
        // If it's a folder then change the visibility to the children (That have the same link).
436
        if ($isRecursive && null !== $link) {
437
            $repo->copyVisibilityToChildren($resource->getResourceNode(), $link);
438
        }
439
440
        $resourceNode->setUpdatedAt(new DateTime());
441
442
        return $resource;
443
    }
444
445
    private function saveZipContentsAsDocuments(array $folderStructure, EntityManager $em, $resourceLinkList = [], $parentResourceId = null, $currentPath = '', $extractPath = '', &$processedItems = []): array
446
    {
447
        $documents = [];
448
449
        foreach ($folderStructure as $key => $item) {
450
            if (is_array($item)) {
451
                $folderName = $key;
452
                $subFolderStructure = $item;
453
454
                $document = new CDocument();
455
                $document->setTitle($folderName);
456
                $document->setFiletype('folder');
457
458
                if ($parentResourceId !== null) {
459
                    $document->setParentResourceNode($parentResourceId);
460
                }
461
462
                if (!empty($resourceLinkList)) {
463
                    $document->setResourceLinkArray($resourceLinkList);
464
                }
465
466
                $em->persist($document);
467
                $em->flush();
468
469
                $documentId = $document->getResourceNode()->getId();
470
                $documents[$documentId] = [
471
                    'name' => $document->getTitle(),
472
                    'files' => []
473
                ];
474
475
                $subDocuments = $this->saveZipContentsAsDocuments($subFolderStructure, $em, $resourceLinkList, $documentId, $currentPath . $folderName . '/', $extractPath, $processedItems);
476
                $documents[$documentId]['files'] = $subDocuments;
477
            } else {
478
                $fileName = $item;
479
480
                $document = new CDocument();
481
                $document->setTitle($fileName);
482
                $document->setFiletype('file');
483
484
                if ($parentResourceId !== null) {
485
                    $document->setParentResourceNode($parentResourceId);
486
                }
487
488
                if (!empty($resourceLinkList)) {
489
                    $document->setResourceLinkArray($resourceLinkList);
490
                }
491
492
                $filePath = $extractPath . '/'. $currentPath . $fileName;
493
494
                if (file_exists($filePath)) {
495
                    $uploadedFile = new UploadedFile(
496
                        $filePath,
497
                        $fileName
498
                    );
499
500
                    $document->setUploadFile($uploadedFile);
501
                    $em->persist($document);
502
                    $em->flush();
503
504
                    $documentId = $document->getResourceNode()->getId();
505
                    $documents[$documentId] = [
506
                        'name' => $document->getTitle(),
507
                        'files' => []
508
                    ];
509
                } else {
510
                    error_log('File does not exist: ' . $filePath);
511
                    continue;
512
                }
513
            }
514
        }
515
516
        return $documents;
517
    }
518
519
    private function extractZipFile(UploadedFile $file, KernelInterface $kernel): array
520
    {
521
        // Get the temporary path of the ZIP file
522
        $zipFilePath = $file->getRealPath();
523
524
        // Create an instance of the ZipArchive class
525
        $zip = new \ZipArchive();
526
        $zip->open($zipFilePath);
527
528
        $cacheDirectory = $kernel->getCacheDir();
529
        $extractPath = $cacheDirectory . '/' . uniqid('extracted_', true);
530
        mkdir($extractPath);
531
532
        // Extract the contents of the ZIP file
533
        $zip->extractTo($extractPath);
534
535
        // Array to store the sorted extracted paths
536
        $extractedPaths = [];
537
538
        // Iterate over each file or directory in the ZIP file
539
        for ($i = 0; $i < $zip->numFiles; $i++) {
540
            $filename = $zip->getNameIndex($i);
541
            $extractedPaths[] = $extractPath . '/' . $filename;
542
        }
543
544
        // Close the ZIP file
545
        $zip->close();
546
547
        // Build the folder structure and file associations
548
        $folderStructure = $this->buildFolderStructure($extractedPaths, $extractPath);
549
550
        // Return the array of folder structure and the extraction path
551
        return [
552
            'folderStructure' => $folderStructure,
553
            'extractPath' => $extractPath
554
        ];
555
    }
556
557
    private function buildFolderStructure(array $paths, string $extractPath): array
558
    {
559
        $folderStructure = [];
560
561
        foreach ($paths as $path) {
562
            $relativePath = str_replace($extractPath . '/', '', $path);
563
            $parts = explode('/', $relativePath);
564
565
            $currentLevel = &$folderStructure;
566
567
            foreach ($parts as $part) {
568
                if (!isset($currentLevel[$part])) {
569
                    $currentLevel[$part] = [];
570
                }
571
572
                $currentLevel = &$currentLevel[$part];
573
            }
574
        }
575
576
        return $this->formatFolderStructure($folderStructure);
577
    }
578
579
    private function formatFolderStructure(array $folderStructure): array
580
    {
581
        $result = [];
582
583
        foreach ($folderStructure as $folder => $contents) {
584
            $formattedContents = $this->formatFolderStructure($contents);
585
586
            if (!empty($formattedContents)) {
587
                $result[$folder] = $formattedContents;
588
            } elseif (!empty($folder)) {
589
                $result[] = $folder;
590
            }
591
        }
592
593
        return $result;
594
    }
595
596
    private function generateUniqueTitle(string $title): string
597
    {
598
        $uniqueTitle = $title . '_' . uniqid();
599
600
        return $uniqueTitle;
601
    }
602
603
}
604