Skip to content

Commit 2b0c8bd

Browse files
Internal: Improve structured-file course creation command with permission check and user validation - refs BT#22644
1 parent e5f1cab commit 2b0c8bd

File tree

2 files changed

+46
-29
lines changed

2 files changed

+46
-29
lines changed

src/CoreBundle/Command/CreateCoursesFromStructuredFileCommand.php

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Chamilo\CoreBundle\Command;
88

99
use Chamilo\CoreBundle\Entity\Course;
10+
use Chamilo\CoreBundle\Entity\ResourceNode;
1011
use Chamilo\CoreBundle\Entity\User;
1112
use Chamilo\CoreBundle\Service\CourseService;
1213
use Chamilo\CoreBundle\Settings\SettingsManager;
@@ -18,14 +19,17 @@
1819
use Symfony\Component\Console\Command\Command;
1920
use Symfony\Component\Console\Input\InputArgument;
2021
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Console\Input\InputOption;
2123
use Symfony\Component\Console\Output\OutputInterface;
2224
use Symfony\Component\Console\Style\SymfonyStyle;
2325
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
2426
use Symfony\Component\Finder\Finder;
2527

2628
#[AsCommand(
2729
name: 'app:create-courses-from-structured-file',
28-
description: 'Create courses and learning paths from a folder containing files',
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',
2933
)]
3034
class CreateCoursesFromStructuredFileCommand extends Command
3135
{
@@ -40,16 +44,33 @@ public function __construct(
4044

4145
protected function configure(): void
4246
{
43-
$this->addArgument(
44-
'folder',
45-
InputArgument::REQUIRED,
46-
'Absolute path to the folder that contains course files'
47-
);
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+
);
4860
}
4961

5062
protected function execute(InputInterface $input, OutputInterface $output): int
5163
{
52-
$io = new SymfonyStyle($input, $output);
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+
5374
$adminUser = $this->getFirstAdmin();
5475
if (!$adminUser) {
5576
$io->error('No admin user found in the system.');
@@ -63,12 +84,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6384
}
6485

6586
// Retrieve Unix permissions from platform settings
66-
$dirPermOct = octdec(
67-
$this->settingsManager->getSetting('document.permissions_for_new_directories') ?? '0777'
68-
);
69-
$filePermOct = octdec(
70-
$this->settingsManager->getSetting('document.permissions_for_new_files') ?? '0666'
71-
);
87+
$dirPermOct = octdec($this->settingsManager->getSetting('document.permissions_for_new_directories') ?? '0777');
88+
$filePermOct = octdec($this->settingsManager->getSetting('document.permissions_for_new_files') ?? '0666');
7289

7390
// Absolute base to /var/upload/resource
7491
$uploadBase = $this->parameterBag->get('kernel.project_dir') . '/var/upload/resource';
@@ -77,9 +94,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7794
$finder->files()->in($folder);
7895

7996
foreach ($finder as $file) {
80-
$basename = $file->getBasename();
97+
$basename = $file->getBasename();
8198
$courseCode = pathinfo($basename, PATHINFO_FILENAME);
82-
$filePath = $file->getRealPath();
99+
$filePath = $file->getRealPath();
83100

84101
// 1. Skip unsupported file extensions
85102
$allowedExtensions = ['pdf', 'html', 'htm', 'mp4'];
@@ -92,15 +109,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
92109

93110
// 2. Create course
94111
$course = $this->courseService->createCourse([
95-
'title' => $courseCode,
96-
'wanted_code' => $courseCode,
112+
'title' => $courseCode,
113+
'wanted_code' => $courseCode,
97114
'add_user_as_teacher' => true,
98-
'course_language' => $this->settingsManager->getSetting('language.platform_language'),
99-
'visibility' => Course::OPEN_PLATFORM,
100-
'subscribe' => true,
101-
'unsubscribe' => true,
102-
'disk_quota' => $this->settingsManager->getSetting('document.default_document_quotum'),
103-
'expiration_date' => (new \DateTime('+1 year'))->format('Y-m-d H:i:s'),
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'),
104121
]);
105122

106123
if (!$course) {
@@ -139,9 +156,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
139156

140157
// 4.1 Apply permissions to the real file & its directory
141158
if ($resourceFile) {
142-
$resourceNodeRepo = $this->em->getRepository(\Chamilo\CoreBundle\Entity\ResourceNode::class);
143-
$relativePath = $resourceNodeRepo->getFilename($resourceFile); // e.g. /2025/06/16/abc.pdf
144-
$fullPath = realpath($uploadBase . $relativePath);
159+
$resourceNodeRepo = $this->em->getRepository(ResourceNode::class);
160+
$relativePath = $resourceNodeRepo->getFilename($resourceFile);
161+
$fullPath = realpath($uploadBase . $relativePath);
145162

146163
if ($fullPath && is_file($fullPath)) {
147164
@chmod($fullPath, $filePermOct);
@@ -154,7 +171,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
154171

155172
// 5. Ensure learning path root item exists
156173
$lpItemRepo = $this->em->getRepository(CLpItem::class);
157-
$rootItem = $lpItemRepo->getRootItem((int) $lp->getIid());
174+
$rootItem = $lpItemRepo->getRootItem((int) $lp->getIid());
158175

159176
if (!$rootItem) {
160177
$rootItem = (new CLpItem())

src/CoreBundle/Settings/DocumentSettingsSchema.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public function buildSettings(AbstractSettingsBuilder $builder): void
2828
'upload_extensions_whitelist' => 'htm;html;jpg;jpeg;gif;png;swf;avi;mpg;mpeg;mov;flv;doc;docx;xls;xlsx;ppt;pptx;odt;odp;ods;pdf;webm;oga;ogg;ogv;h264',
2929
'upload_extensions_skip' => 'true',
3030
'upload_extensions_replace_by' => 'dangerous',
31-
'permissions_for_new_directories' => '0777',
32-
'permissions_for_new_files' => '0666',
31+
'permissions_for_new_directories' => '0770',
32+
'permissions_for_new_files' => '0660',
3333
'show_glossary_in_documents' => 'none',
3434
'students_download_folders' => 'true',
3535
'users_copy_files' => 'true',

0 commit comments

Comments
 (0)