@@ -258,25 +258,59 @@ public static function getAvailableBackups($user_id = null)
258258 }
259259
260260 /**
261- * @param array $file
261+ * @param array|string $file Either $_FILES['...'] array or a tmp path string
262262 *
263- * @return bool|string
263+ * @return bool|string Returns the stored zip filename (not full path) or false on failure
264264 */
265265 public static function importUploadedFile ($ file )
266266 {
267- $ new_filename = uniqid ('import_file ' , true ).'.zip ' ;
268- $ new_dir = self ::getBackupDir ();
269- if (!is_dir ($ new_dir )) {
270- $ fs = new Filesystem ();
271- $ fs ->mkdir ($ new_dir );
267+ $ newFilename = uniqid ('import_file ' , true ).'.zip ' ;
268+ $ newDir = self ::getBackupDir ();
269+
270+ // Ensure backup directory exists
271+ if (!is_dir ($ newDir )) {
272+ @mkdir ($ newDir , api_get_permissions_for_new_directories (), true );
273+ }
274+
275+ if (!is_dir ($ newDir ) || !is_writable ($ newDir )) {
276+ return false ;
277+ }
278+
279+ // Normalize input
280+ $ tmpPath = '' ;
281+ $ uploadErr = 0 ;
282+
283+ if (is_array ($ file )) {
284+ $ uploadErr = (int ) ($ file ['error ' ] ?? 0 );
285+ $ tmpPath = (string ) ($ file ['tmp_name ' ] ?? '' );
286+ } else {
287+ $ tmpPath = (string ) $ file ;
272288 }
273- if (is_dir ($ new_dir ) && is_writable ($ new_dir )) {
274- move_uploaded_file ($ file , $ new_dir .$ new_filename );
275289
276- return $ new_filename ;
290+ if ($ uploadErr !== 0 || $ tmpPath === '' ) {
291+ return false ;
277292 }
278293
279- return false ;
294+ $ destPath = $ newDir .$ newFilename ;
295+
296+ // Prefer move_uploaded_file for real HTTP uploads, fallback to copy for non-upload contexts
297+ $ ok = false ;
298+ if (function_exists ('is_uploaded_file ' ) && is_uploaded_file ($ tmpPath )) {
299+ $ ok = @move_uploaded_file ($ tmpPath , $ destPath );
300+ } elseif (is_file ($ tmpPath ) && is_readable ($ tmpPath )) {
301+ $ ok = @copy ($ tmpPath , $ destPath );
302+ } else {
303+ return false ;
304+ }
305+
306+ clearstatcache (true , $ destPath );
307+
308+ if (!$ ok || !is_file ($ destPath ) || (int ) filesize ($ destPath ) <= 0 ) {
309+ @unlink ($ destPath );
310+ return false ;
311+ }
312+
313+ return $ newFilename ;
280314 }
281315
282316 /**
@@ -297,36 +331,73 @@ public static function readCourse($filename, $delete = false)
297331 $ unzip_dir = self ::getBackupDir ().$ tmp_dir_name ;
298332 $ filePath = self ::getBackupDir ().$ filename ;
299333
300- @mkdir ($ unzip_dir , api_get_permissions_for_new_directories (), true );
301- @copy (
302- $ filePath ,
303- $ unzip_dir .'/backup.zip '
304- );
334+ $ perms = api_get_permissions_for_new_directories ();
335+
336+ if (!is_dir ($ unzip_dir ) && !@mkdir ($ unzip_dir , $ perms , true )) {
337+ error_log ('[COURSE_ARCHIVER] readCourse: failed to create unzip_dir=" ' .$ unzip_dir .'" ' );
338+ return new Course ();
339+ }
340+
341+ if (!is_file ($ filePath )) {
342+ error_log ('[COURSE_ARCHIVER] readCourse: backup zip not found filePath=" ' .$ filePath .'" ' );
343+ return new Course ();
344+ }
305345
306- // unzip the archive
346+ if (!@copy ($ filePath , $ unzip_dir .'/backup.zip ' )) {
347+ error_log ('[COURSE_ARCHIVER] readCourse: failed to copy zip filePath=" ' .$ filePath .'" to " ' .$ unzip_dir .'/backup.zip" ' );
348+ return new Course ();
349+ }
350+
351+ // Unzip the archive
307352 $ zip = new \PclZip ($ unzip_dir .'/backup.zip ' );
308- @chdir ($ unzip_dir );
353+
354+ if (!@chdir ($ unzip_dir )) {
355+ error_log ('[COURSE_ARCHIVER] readCourse: chdir failed unzip_dir=" ' .$ unzip_dir .'" ' );
356+ return new Course ();
357+ }
309358
310359 // For course backups we must preserve original filenames so that
311360 // paths in course_info.dat still match the files in backup_path.
312- $ zip ->extract (
313- PCLZIP_OPT_TEMP_FILE_ON
314- );
361+ $ extractResult = $ zip ->extract (PCLZIP_OPT_TEMP_FILE_ON );
362+
363+ if ($ extractResult === 0 ) {
364+ error_log ('[COURSE_ARCHIVER] readCourse: extract failed error=" ' .$ zip ->errorInfo (true ).'" unzip_dir=" ' .$ unzip_dir .'" ' );
365+ return new Course ();
366+ }
315367
316- // remove the archive-file
368+ // Remove the archive-file
317369 if ($ delete ) {
318370 @unlink ($ filePath );
319371 }
320372
321- // read the course
373+ // Read the course
322374 if (!is_file ('course_info.dat ' )) {
375+ error_log ('[COURSE_ARCHIVER] readCourse: missing course_info.dat cwd=" ' .getcwd ().'" unzip_dir=" ' .$ unzip_dir .'" ' );
376+ return new Course ();
377+ }
378+
379+ $ size = (int ) @filesize ('course_info.dat ' );
380+ if ($ size <= 0 ) {
381+ error_log ('[COURSE_ARCHIVER] readCourse: empty course_info.dat size= ' .$ size .' cwd=" ' .getcwd ().'" ' );
323382 return new Course ();
324383 }
325384
326- $ fp = @fopen ('course_info.dat ' , "r " );
327- $ contents = @fread ($ fp , filesize ('course_info.dat ' ));
385+ $ fp = @fopen ('course_info.dat ' , 'r ' );
386+ if (false === $ fp ) {
387+ error_log ('[COURSE_ARCHIVER] readCourse: failed to open course_info.dat cwd=" ' .getcwd ().'" ' );
388+ return new Course ();
389+ }
390+
391+ $ contents = @fread ($ fp , $ size );
328392 @fclose ($ fp );
329393
394+ $ readLen = is_string ($ contents ) ? strlen ($ contents ) : -1 ;
395+ if (!is_string ($ contents ) || $ readLen <= 0 ) {
396+ error_log ('[COURSE_ARCHIVER] readCourse: failed to read course_info.dat ' );
397+ return new Course ();
398+ }
399+
400+ // Backward compatibility aliases used by serialized payloads
330401 class_alias ('Chamilo\CourseBundle\Component\CourseCopy\Course ' , 'Course ' );
331402 class_alias ('Chamilo\CourseBundle\Component\CourseCopy\Resources\Announcement ' , 'Announcement ' );
332403 class_alias ('Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance ' , 'Attendance ' );
@@ -357,17 +428,25 @@ class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki', 'Wiki');
357428 class_alias ('Chamilo\CourseBundle\Component\CourseCopy\Resources\Work ' , 'Work ' );
358429 class_alias ('Chamilo\CourseBundle\Component\CourseCopy\Resources\XapiTool ' , 'XapiTool ' );
359430
360- /** @var Course $course */
361- $ course = \UnserializeApi::unserialize ('course ' , base64_decode ($ contents ));
431+ $ decoded = base64_decode ($ contents , true );
432+ if (false === $ decoded ) {
433+ error_log ('[COURSE_ARCHIVER] readCourse: base64_decode strict failed, retry non-strict ' );
434+ $ decoded = base64_decode ($ contents );
435+ }
436+
437+ if (!is_string ($ decoded ) || $ decoded === '' ) {
438+ error_log ('[COURSE_ARCHIVER] readCourse: base64_decode produced empty payload ' );
439+ return new Course ();
440+ }
362441
363- if (!in_array (
364- get_class ($ course ),
365- ['Course ' , 'Chamilo\CourseBundle\Component\CourseCopy\Course ' ]
366- )
367- ) {
442+ /** @var mixed $course */
443+ $ course = \UnserializeApi::unserialize ('course ' , $ decoded );
444+ if (!is_object ($ course ) || !in_array (get_class ($ course ), ['Course ' , 'Chamilo\CourseBundle\Component\CourseCopy\Course ' ], true )) {
445+ error_log ('[COURSE_ARCHIVER] readCourse: invalid class after unserialize, returning empty Course ' );
368446 return new Course ();
369447 }
370448
449+ // Ensure backup_path is always set when unserialize is successful
371450 $ course ->backup_path = $ unzip_dir ;
372451
373452 return $ course ;
0 commit comments