Passed
Pull Request — master (#6351)
by
unknown
09:06
created

configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 0
dl 0
loc 14
rs 9.9
c 2
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Command;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Entity\ResourceNode;
11
use Chamilo\CoreBundle\Entity\User;
12
use Chamilo\CoreBundle\Service\CourseService;
13
use Chamilo\CoreBundle\Settings\SettingsManager;
14
use Chamilo\CourseBundle\Entity\CDocument;
15
use Chamilo\CourseBundle\Entity\CLp;
16
use Chamilo\CourseBundle\Entity\CLpItem;
17
use Doctrine\ORM\EntityManagerInterface;
18
use Symfony\Component\Console\Attribute\AsCommand;
19
use Symfony\Component\Console\Command\Command;
20
use Symfony\Component\Console\Input\InputArgument;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
26
use Symfony\Component\Finder\Finder;
27
28
#[AsCommand(
29
    name: 'app:create-courses-from-structured-file',
30
    description: 'Create courses and learning paths from a folder containing files.
31
If permissions like 0660/0770 are used, it is recommended to run this command as www-data:
32
  sudo -u www-data php bin/console app:create-courses-from-structured-file /path/to/folder',
33
)]
34
class CreateCoursesFromStructuredFileCommand extends Command
35
{
36
    public function __construct(
37
        private readonly EntityManagerInterface $em,
38
        private readonly CourseService $courseService,
39
        private readonly SettingsManager $settingsManager,
40
        private readonly ParameterBagInterface $parameterBag,
41
    ) {
42
        parent::__construct();
43
    }
44
45
    protected function configure(): void
46
    {
47
        $this
48
            ->addArgument(
49
                'folder',
50
                InputArgument::REQUIRED,
51
                'Absolute path to the folder that contains course files'
52
            )
53
            ->addOption(
54
                'user',
55
                null,
56
                InputOption::VALUE_OPTIONAL,
57
                'Expected user owner of created files (e.g. www-data)',
58
                'www-data'
59
            );
60
    }
61
62
    protected function execute(InputInterface $input, OutputInterface $output): int
63
    {
64
        $io = new SymfonyStyle($input, $output);
65
66
        $expectedUser = $input->getOption('user');
67
        $realUser = get_current_user();
68
69
        if ($realUser !== $expectedUser) {
70
            $io->warning("You are running this command as '$realUser', but expected user is '$expectedUser'.If file permissions are too restrictive (e.g. 0660), the web server may not be able to access the files.
71
            To avoid this issue, consider running the command like this: sudo -u {$expectedUser} php bin/console app:create-courses-from-structured-file /path/to/folder");
72
        }
73
74
        $adminUser = $this->getFirstAdmin();
75
        if (!$adminUser) {
76
            $io->error('No admin user found in the system.');
77
            return Command::FAILURE;
78
        }
79
80
        $folder = $input->getArgument('folder');
81
        if (!is_dir($folder)) {
82
            $io->error("Invalid folder: $folder");
83
            return Command::FAILURE;
84
        }
85
86
        // Retrieve Unix permissions from platform settings
87
        $dirPermOct = octdec($this->settingsManager->getSetting('document.permissions_for_new_directories') ?? '0777');
88
        $filePermOct = octdec($this->settingsManager->getSetting('document.permissions_for_new_files') ?? '0666');
89
90
        // Absolute base to /var/upload/resource
91
        $uploadBase = $this->parameterBag->get('kernel.project_dir') . '/var/upload/resource';
92
93
        $finder = new Finder();
94
        $finder->files()->in($folder);
95
96
        foreach ($finder as $file) {
97
            $basename = $file->getBasename();
98
            $courseCode = pathinfo($basename, PATHINFO_FILENAME);
99
            $filePath = $file->getRealPath();
100
101
            // 1. Skip unsupported file extensions
102
            $allowedExtensions = ['pdf', 'html', 'htm', 'mp4'];
103
            if (!in_array(strtolower($file->getExtension()), $allowedExtensions, true)) {
104
                $io->warning("Skipping unsupported file: $basename");
105
                continue;
106
            }
107
108
            $io->section("Creating course: $courseCode");
109
110
            // 2. Create course
111
            $course = $this->courseService->createCourse([
112
                'title' => $courseCode,
113
                'wanted_code' => $courseCode,
114
                'add_user_as_teacher' => true,
115
                'course_language' => $this->settingsManager->getSetting('language.platform_language'),
116
                'visibility' => Course::OPEN_PLATFORM,
117
                'subscribe' => true,
118
                'unsubscribe' => true,
119
                'disk_quota' => $this->settingsManager->getSetting('document.default_document_quotum'),
120
                'expiration_date' => (new \DateTime('+1 year'))->format('Y-m-d H:i:s'),
121
            ]);
122
123
            if (!$course) {
124
                throw new \RuntimeException("Course '$courseCode' could not be created.");
125
            }
126
127
            // 3. Create learning path
128
            $lp = (new CLp())
129
                ->setLpType(1)
130
                ->setTitle($courseCode)
131
                ->setDescription('')
132
                ->setPublishedOn(null)
133
                ->setExpiredOn(null)
134
                ->setCategory(null)
135
                ->setParent($course)
136
                ->addCourseLink($course)
137
                ->setCreator($adminUser);
138
139
            $this->em->getRepository(CLp::class)->createLp($lp);
140
141
            // 4. Create document
142
            $document = (new CDocument())
143
                ->setFiletype('file')
144
                ->setTitle($basename)
145
                ->setComment(null)
146
                ->setReadonly(false)
147
                ->setCreator($adminUser)
148
                ->setParent($course)
149
                ->addCourseLink($course);
150
151
            $this->em->persist($document);
152
            $this->em->flush();
153
154
            $documentRepo = $this->em->getRepository(CDocument::class);
155
            $resourceFile = $documentRepo->addFileFromPath($document, $basename, $filePath);
156
157
            // 4.1  Apply permissions to the real file & its directory
158
            if ($resourceFile) {
159
                $resourceNodeRepo = $this->em->getRepository(ResourceNode::class);
160
                $relativePath = $resourceNodeRepo->getFilename($resourceFile);
161
                $fullPath = realpath($uploadBase . $relativePath);
162
163
                if ($fullPath && is_file($fullPath)) {
164
                    @chmod($fullPath, $filePermOct);
165
                }
166
                $fullDir = dirname($fullPath ?: '');
167
                if ($fullDir && is_dir($fullDir)) {
168
                    @chmod($fullDir, $dirPermOct);
169
                }
170
            }
171
172
            // 5. Ensure learning path root item exists
173
            $lpItemRepo = $this->em->getRepository(CLpItem::class);
174
            $rootItem = $lpItemRepo->getRootItem((int) $lp->getIid());
175
176
            if (!$rootItem) {
177
                $rootItem = (new CLpItem())
178
                    ->setTitle('root')
179
                    ->setPath('root')
180
                    ->setLp($lp)
181
                    ->setItemType('root');
182
                $this->em->persist($rootItem);
183
                $this->em->flush();
184
            }
185
186
            // 6. Create LP item linked to the document
187
            $lpItem = (new CLpItem())
188
                ->setLp($lp)
189
                ->setTitle($basename)
190
                ->setItemType('document')
191
                ->setRef((string) $document->getIid())
192
                ->setPath((string) $document->getIid())
193
                ->setDisplayOrder(1)
194
                ->setLaunchData('')
195
                ->setMinScore(0)
196
                ->setMaxScore(100)
197
                ->setParent($rootItem)
198
                ->setLvl(1)
199
                ->setRoot($rootItem);
200
201
            $this->em->persist($lpItem);
202
            $this->em->flush();
203
204
            $io->success("Course '$courseCode' created with LP and document '$basename'");
205
        }
206
207
        return Command::SUCCESS;
208
    }
209
210
    /**
211
     * Return the first user that has ROLE_ADMIN.
212
     */
213
    private function getFirstAdmin(): ?User
214
    {
215
        return $this->em->getRepository(User::class)
216
            ->createQueryBuilder('u')
217
            ->where('u.roles LIKE :role')
218
            ->setParameter('role', '%ROLE_ADMIN%')
219
            ->setMaxResults(1)
220
            ->getQuery()
221
            ->getOneOrNullResult();
222
    }
223
}
224