Skip to content

Commit 92a8227

Browse files
Learnpath: Enhance SCORM upload quota validation and cleanup - refs #1970
Author: @christianbeeznest
1 parent 7e1a483 commit 92a8227

File tree

5 files changed

+169
-70
lines changed

5 files changed

+169
-70
lines changed

public/main/lp/learnpath.class.php

Lines changed: 87 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -793,8 +793,23 @@ public function delete($courseInfo = null, $id = null, $delete = 'keep')
793793

794794
$course = api_get_course_entity();
795795
$session = api_get_session_entity();
796+
/* @var CLp $lp */
796797
$lp = Container::getLpRepository()->find($this->lp_id);
797798

799+
// 1) Detach the asset to avoid FK constraint
800+
$asset = $lp->getAsset();
801+
if ($asset) {
802+
$lp->setAsset(null);
803+
$em = Database::getManager();
804+
$em->persist($lp);
805+
$em->flush();
806+
}
807+
808+
// 2) Now delete the asset and its folder
809+
if ($asset) {
810+
Container::getAssetRepository()->delete($asset);
811+
}
812+
798813
Database::getManager()
799814
->getRepository(ResourceLink::class)
800815
->removeByResourceInContext($lp, $course, $session);
@@ -7051,40 +7066,89 @@ public function copy()
70517066
);
70527067
}
70537068

7054-
/**
7055-
* Verify document size.
7056-
*
7057-
* @param string $s
7058-
*
7059-
* @return bool
7060-
*/
7061-
public static function verify_document_size($s)
7069+
public static function getQuotaInfo(string $localFilePath): array
70627070
{
7063-
$post_max = ini_get('post_max_size');
7064-
if ('M' == substr($post_max, -1, 1)) {
7065-
$post_max = intval(substr($post_max, 0, -1)) * 1024 * 1024;
7066-
} elseif ('G' == substr($post_max, -1, 1)) {
7067-
$post_max = intval(substr($post_max, 0, -1)) * 1024 * 1024 * 1024;
7071+
$post_max_raw = ini_get('post_max_size');
7072+
$post_max_bytes = (int) rtrim($post_max_raw, 'MG') * (str_ends_with($post_max_raw,'G') ? 1024**3 : 1024**2);
7073+
$upload_max_raw = ini_get('upload_max_filesize');
7074+
$upload_max_bytes = (int) rtrim($upload_max_raw, 'MG') * (str_ends_with($upload_max_raw,'G') ? 1024**3 : 1024**2);
7075+
7076+
$em = Database::getManager();
7077+
$course = api_get_course_entity(api_get_course_int_id());
7078+
7079+
$nodes = Container::getResourceNodeRepository()->findByResourceTypeAndCourse('file', $course);
7080+
$root = null;
7081+
foreach ($nodes as $n) {
7082+
if ($n->getParent() === null) {
7083+
$root = $n; break;
7084+
}
70687085
}
7069-
$upl_max = ini_get('upload_max_filesize');
7070-
if ('M' == substr($upl_max, -1, 1)) {
7071-
$upl_max = intval(substr($upl_max, 0, -1)) * 1024 * 1024;
7072-
} elseif ('G' == substr($upl_max, -1, 1)) {
7073-
$upl_max = intval(substr($upl_max, 0, -1)) * 1024 * 1024 * 1024;
7086+
$docsSize = $root
7087+
? Container::getDocumentRepository()->getFolderSize($root, $course)
7088+
: 0;
7089+
7090+
$assetRepo = Container::getAssetRepository();
7091+
$fs = $assetRepo->getFileSystem();
7092+
$scormSize = 0;
7093+
foreach (Container::getLpRepository()->findScormByCourse($course) as $lp) {
7094+
if ($asset = $lp->getAsset()) {
7095+
$folder = $assetRepo->getFolder($asset);
7096+
if ($folder && $fs->directoryExists($folder)) {
7097+
$scormSize += self::getFolderSize($folder);
7098+
}
7099+
}
70747100
}
70757101

7076-
$repo = Container::getDocumentRepository();
7077-
$documents_total_space = $repo->getTotalSpace(api_get_course_int_id());
7102+
$uploadedSize = filesize($localFilePath);
7103+
$existingTotal = $docsSize + $scormSize;
7104+
$combined = $existingTotal + $uploadedSize;
7105+
7106+
$quotaMb = DocumentManager::get_course_quota();
7107+
$quotaBytes = $quotaMb * 1024 * 1024;
70787108

7079-
$course_max_space = DocumentManager::get_course_quota();
7080-
$total_size = filesize($s) + $documents_total_space;
7081-
if (filesize($s) > $post_max || filesize($s) > $upl_max || $total_size > $course_max_space) {
7109+
return [
7110+
'post_max' => $post_max_bytes,
7111+
'upload_max' => $upload_max_bytes,
7112+
'docs_size' => $docsSize,
7113+
'scorm_size' => $scormSize,
7114+
'existing_total'=> $existingTotal,
7115+
'uploaded_size' => $uploadedSize,
7116+
'combined' => $combined,
7117+
'quota_bytes' => $quotaBytes,
7118+
];
7119+
}
7120+
7121+
/**
7122+
* Verify document size.
7123+
*/
7124+
public static function verify_document_size(string $localFilePath): bool
7125+
{
7126+
$info = self::getQuotaInfo($localFilePath);
7127+
if ($info['uploaded_size'] > $info['post_max']
7128+
|| $info['uploaded_size'] > $info['upload_max']
7129+
|| $info['combined'] > $info['quota_bytes']
7130+
) {
7131+
Container::getSession()->set('quota_info', $info);
70827132
return true;
70837133
}
70847134

70857135
return false;
70867136
}
70877137

7138+
private static function getFolderSize(string $path): int
7139+
{
7140+
$size = 0;
7141+
$iterator = new \RecursiveIteratorIterator(
7142+
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
7143+
);
7144+
foreach ($iterator as $file) {
7145+
if ($file->isFile()) {
7146+
$size += $file->getSize();
7147+
}
7148+
}
7149+
return $size;
7150+
}
7151+
70887152
/**
70897153
* Clear LP prerequisites.
70907154
*/

public/main/lp/lp_upload.php

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/* For licensing terms, see /license.txt */
44

5+
use Chamilo\CoreBundle\Helpers\ChamiloHelper;
56
use Chamilo\CourseBundle\Component\CourseCopy\CourseArchiver;
67
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
78

@@ -29,56 +30,54 @@
2930
* because if the file size exceed the maximum file upload
3031
* size set in php.ini, all variables from POST are cleared !
3132
*/
32-
$user_file = $_GET['user_file'] ?? [];
33-
$user_file = $user_file ? $user_file : [];
34-
$is_error = $user_file['error'] ?? false;
35-
$em = Database::getManager();
33+
$user_file = $_FILES['user_file'] ?? [];
34+
$is_error = $user_file['error'] ?? false;
35+
$em = Database::getManager();
3636

3737
if (isset($_POST) && $is_error) {
38-
Display::addFlash(
39-
Display::return_message(get_lang('The file is too big to upload.'))
40-
);
41-
42-
return false;
4338
unset($_FILES['user_file']);
44-
} elseif ('POST' === $_SERVER['REQUEST_METHOD'] && count($_FILES) > 0 && !empty($_FILES['user_file']['name'])) {
39+
ChamiloHelper::redirectTo(api_get_path(WEB_PATH).'main/upload/index.php?'.api_get_cidreq().'&origin=course&curdirpath=/&tool=learnpath');
40+
}
41+
elseif ('POST' === $_SERVER['REQUEST_METHOD']
42+
&& !empty($_FILES['user_file']['name'])
43+
) {
4544
// A file upload has been detected, now deal with the file...
46-
// Directory creation.
47-
$stopping_error = false;
4845
$s = $_FILES['user_file']['name'];
4946

5047
// Get name of the zip file without the extension.
51-
$info = pathinfo($s);
52-
$filename = $info['basename'];
53-
$extension = $info['extension'];
48+
$info = pathinfo($s);
49+
$filename = $info['basename'];
50+
$extension = $info['extension'];
5451
$file_base_name = str_replace('.'.$extension, '', $filename);
5552

5653
$new_dir = api_replace_dangerous_char(trim($file_base_name));
57-
$type = learnpath::getPackageType($_FILES['user_file']['tmp_name'], $_FILES['user_file']['name']);
58-
59-
$proximity = 'local';
60-
if (!empty($_REQUEST['content_proximity'])) {
61-
$proximity = $_REQUEST['content_proximity'];
62-
}
54+
$type = learnpath::getPackageType(
55+
$_FILES['user_file']['tmp_name'],
56+
$_FILES['user_file']['name']
57+
);
6358

64-
$maker = 'Scorm';
65-
if (!empty($_REQUEST['content_maker'])) {
66-
$maker = $_REQUEST['content_maker'];
67-
}
59+
// Defaults
60+
$proximity = $_REQUEST['content_proximity'] ?? 'local';
61+
$maker = $_REQUEST['content_maker'] ?? 'Scorm';
6862

6963
switch ($type) {
7064
case 'chamilo':
7165
$filename = CourseArchiver::importUploadedFile($_FILES['user_file']['tmp_name']);
7266
if ($filename) {
73-
$course = CourseArchiver::readCourse($filename, false);
67+
$course = CourseArchiver::readCourse($filename, false);
7468
$courseRestorer = new CourseRestorer($course);
75-
// FILE_SKIP, FILE_RENAME or FILE_OVERWRITE
7669
$courseRestorer->set_file_option(FILE_OVERWRITE);
7770
$courseRestorer->restore('', api_get_session_id());
7871
Display::addFlash(Display::return_message(get_lang('File upload succeeded!')));
7972
}
8073
break;
8174
case 'scorm':
75+
$tmpFile = $_FILES['user_file']['tmp_name'];
76+
$fileSize = filesize($tmpFile);
77+
$blocked = learnpath::verify_document_size($tmpFile);
78+
if ($blocked) {
79+
ChamiloHelper::redirectTo(api_get_path(WEB_PATH).'main/upload/index.php?'.api_get_cidreq().'&origin=course&curdirpath=/&tool=learnpath');
80+
}
8281
$scorm = new scorm();
8382
$scorm->import_package(
8483
$_FILES['user_file'],
@@ -92,10 +91,8 @@
9291
$scorm->parse_manifest();
9392
$lp = $scorm->import_manifest(api_get_course_int_id(), $_REQUEST['use_max_score']);
9493
if ($lp) {
95-
$lp
96-
->setContentLocal($proximity)
97-
->setContentMaker($maker)
98-
;
94+
$lp->setContentLocal($proximity)
95+
->setContentMaker($maker);
9996
$em->persist($lp);
10097
$em->flush();
10198
Display::addFlash(Display::return_message(get_lang('File upload succeeded!')));
@@ -104,31 +101,25 @@
104101
break;
105102
case 'aicc':
106103
$oAICC = new aicc();
107-
//$entity = $oAICC->getEntity();
108104
$config_dir = $oAICC->import_package($_FILES['user_file']);
109105
if (!empty($config_dir)) {
110106
$oAICC->parse_config_files($config_dir);
111107
$oAICC->import_aicc(api_get_course_id());
112108
Display::addFlash(Display::return_message(get_lang('File upload succeeded!')));
113109
}
114-
/*$entity
115-
->setContentLocal($proximity)
116-
->setContentMaker($maker)
117-
->setJsLib('aicc_api.php')
118-
;
119-
$em->persist($entity);
120-
$em->flush();*/
121110
break;
122111
case 'oogie':
123-
$take_slide_name = empty($_POST['take_slide_name']) ? false : true;
112+
$take_slide_name = !empty($_POST['take_slide_name']);
124113
$o_ppt = new OpenofficePresentation($take_slide_name);
125-
$first_item_id = $o_ppt->convert_document($_FILES['user_file'], 'make_lp', $_POST['slide_size']);
114+
$o_ppt->convert_document($_FILES['user_file'], 'make_lp', $_POST['slide_size']);
126115
Display::addFlash(Display::return_message(get_lang('File upload succeeded!')));
127116
break;
128117
case 'woogie':
129-
$split_steps = empty($_POST['split_steps']) || 'per_page' === $_POST['split_steps'] ? 'per_page' : 'per_chapter';
118+
$split_steps = (!empty($_POST['split_steps']) && $_POST['split_steps'] === 'per_chapter')
119+
? 'per_chapter'
120+
: 'per_page';
130121
$o_doc = new OpenofficeText($split_steps);
131-
$first_item_id = $o_doc->convert_document($_FILES['user_file']);
122+
$o_doc->convert_document($_FILES['user_file']);
132123
Display::addFlash(Display::return_message(get_lang('File upload succeeded!')));
133124
break;
134125
case '':
@@ -235,6 +226,5 @@
235226
);
236227

237228
return false;
238-
break;
239229
}
240230
}

public/main/upload/index.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,20 @@ function check_unzip() {
167167
'normal',
168168
false
169169
);
170+
171+
$quotaInfo = Session::read('quota_info');
172+
if (!empty($quotaInfo)) {
173+
$msg = sprintf(
174+
"Upload failed: file %d bytes, PHP post_max=%d, upload_max=%d, total after upload=%d, quota=%d",
175+
$quotaInfo['uploaded_size'],
176+
$quotaInfo['post_max'],
177+
$quotaInfo['upload_max'],
178+
$quotaInfo['combined'],
179+
$quotaInfo['quota_bytes']
180+
);
181+
echo Display::return_message(htmlentities($msg), 'error');
182+
Session::erase('quota_info');
183+
}
170184
$form->display();
171185

172186
Display::display_footer();

src/CoreBundle/Repository/AssetRepository.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,27 @@ public function update(Asset $asset): void
161161
$this->getEntityManager()->flush();
162162
}
163163

164+
/**
165+
* Deletes an Asset from the database.
166+
* If it is a SCORM package, first removes its extracted folder on disk.
167+
*/
164168
public function delete(?Asset $asset = null): void
165169
{
166-
if (null !== $asset) {
167-
$this->getEntityManager()->remove($asset);
168-
$this->getEntityManager()->flush();
170+
if (null === $asset) {
171+
return;
172+
}
173+
174+
// If this is a SCORM package, delete its directory and all its contents
175+
if (Asset::SCORM === $asset->getCategory()) {
176+
$folder = $this->getFolder($asset); // e.g. "/scorm/MyScormPackage/"
177+
if ($folder && $this->filesystem->directoryExists($folder)) {
178+
$this->filesystem->deleteDirectory($folder);
179+
}
169180
}
181+
182+
// Remove the asset record from the database
183+
$em = $this->getEntityManager();
184+
$em->remove($asset);
185+
$em->flush();
170186
}
171187
}

src/CourseBundle/Repository/CLpRepository.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ public function getLpSessionId(int $lpId): ?int
140140
return null;
141141
}
142142

143+
public function findScormByCourse(Course $course): array
144+
{
145+
return $this->createQueryBuilder('lp')
146+
->innerJoin('lp.resourceNode', 'rn')
147+
->innerJoin('rn.resourceLinks', 'rl')
148+
->andWhere('rl.course = :course')
149+
->andWhere('lp.lpType = :scormType')
150+
->setParameters([
151+
'course' => $course,
152+
'scormType' => CLp::SCORM_TYPE
153+
])
154+
->getQuery()
155+
->getResult();
156+
}
157+
143158
public function lastProgressForUser(iterable $lps, User $user, ?Session $session): array
144159
{
145160
$lpIds = [];

0 commit comments

Comments
 (0)