Passed
Pull Request — 1.11.x (#7472)
by
unknown
09:59
created

CourseArchiver::importUploadedFile()   C

Complexity

Conditions 14
Paths 26

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 28
c 1
b 0
f 0
dl 0
loc 49
rs 6.2666
cc 14
nc 26
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/* For licensing terms, see /license.txt */
3
4
namespace Chamilo\CourseBundle\Component\CourseCopy;
5
6
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Asset;
7
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Document;
8
use Symfony\Component\Filesystem\Filesystem;
9
10
/**
11
 * Some functions to write a course-object to a zip-file and to read a course-
12
 * object from such a zip-file.
13
 *
14
 * @author Bart Mollet <[email protected]>
15
 *
16
 * @package chamilo.backup
17
 *
18
 * @todo Use archive-folder of Chamilo?
19
 */
20
class CourseArchiver
21
{
22
    /**
23
     * @return string
24
     */
25
    public static function getBackupDir()
26
    {
27
        return api_get_path(SYS_ARCHIVE_PATH).'course_backups/';
28
    }
29
30
    /**
31
     * @return string
32
     */
33
    public static function createBackupDir()
34
    {
35
        $perms = api_get_permissions_for_new_directories();
36
        $dir = self::getBackupDir();
37
        $fs = new Filesystem();
38
        $fs->mkdir($dir, $perms);
39
40
        return $dir;
41
    }
42
43
    /**
44
     * Delete old temp-dirs.
45
     */
46
    public static function cleanBackupDir()
47
    {
48
        $dir = self::getBackupDir();
49
        if (is_dir($dir)) {
50
            if ($handle = @opendir($dir)) {
51
                while (($file = readdir($handle)) !== false) {
52
                    if ($file != "." && $file != ".." &&
53
                        strpos($file, 'CourseArchiver_') === 0 &&
54
                        is_dir($dir.'/'.$file)
55
                    ) {
56
                        rmdirr($dir.'/'.$file);
57
                    }
58
                }
59
                closedir($handle);
60
            }
61
        }
62
    }
63
64
    /**
65
     * Write a course and all its resources to a zip-file.
66
     *
67
     * @return string A pointer to the zip-file
68
     */
69
    public static function createBackup($course)
70
    {
71
        self::cleanBackupDir();
72
        self::createBackupDir();
73
74
        $perm_dirs = api_get_permissions_for_new_directories();
75
        $backupDirectory = self::getBackupDir();
76
77
        // Create a temp directory
78
        $backup_dir = $backupDirectory.'CourseArchiver_'.api_get_unique_id().'/';
79
80
        // All course-information will be stored in course_info.dat
81
        $course_info_file = $backup_dir.'course_info.dat';
82
83
        $user = api_get_user_info();
84
        $date = new \DateTime(api_get_local_time());
85
        $zipFileName = $user['user_id'].'_'.$course->code.'_'.$date->format('Ymd-His').'.zip';
86
        $zipFilePath = $backupDirectory.$zipFileName;
87
88
        $php_errormsg = '';
89
        $res = @mkdir($backup_dir, $perm_dirs);
90
        if ($res === false) {
91
            //TODO set and handle an error message telling the user to review the permissions on the archive directory
92
            error_log(__FILE__.' line '.__LINE__.': '.(ini_get('track_errors') != false ? $php_errormsg : 'error not recorded because track_errors is off in your php.ini').' - This error, occuring because your archive directory will not let this script write data into it, will prevent courses backups to be created', 0);
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing ini_get('track_errors') of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
93
        }
94
        // Write the course-object to the file
95
        $fp = @fopen($course_info_file, 'w');
96
        if ($fp === false) {
97
            error_log(__FILE__.' line '.__LINE__.': '.(ini_get('track_errors') != false ? $php_errormsg : 'error not recorded because track_errors is off in your php.ini'), 0);
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing ini_get('track_errors') of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
98
        }
99
100
        $res = @fwrite($fp, base64_encode(serialize($course)));
101
        if ($res === false) {
102
            error_log(__FILE__.' line '.__LINE__.': '.(ini_get('track_errors') != false ? $php_errormsg : 'error not recorded because track_errors is off in your php.ini'), 0);
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing ini_get('track_errors') of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
103
        }
104
105
        $res = @fclose($fp);
106
        if ($res === false) {
107
            error_log(__FILE__.' line '.__LINE__.': '.(ini_get('track_errors') != false ? $php_errormsg : 'error not recorded because track_errors is off in your php.ini'), 0);
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing ini_get('track_errors') of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
108
        }
109
110
        // Copy all documents to the temp-dir
111
        if (isset($course->resources[RESOURCE_DOCUMENT]) && is_array($course->resources[RESOURCE_DOCUMENT])) {
112
            $webEditorCss = api_get_path(WEB_CSS_PATH).'editor.css';
113
            /** @var Document $document */
114
            foreach ($course->resources[RESOURCE_DOCUMENT] as $document) {
115
                if ($document->file_type == DOCUMENT) {
116
                    $doc_dir = $backup_dir.$document->path;
117
                    @mkdir(dirname($doc_dir), $perm_dirs, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

117
                    /** @scrutinizer ignore-unhandled */ @mkdir(dirname($doc_dir), $perm_dirs, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
118
                    if (file_exists($course->path.$document->path)) {
119
                        copy($course->path.$document->path, $doc_dir);
120
                        // Check if is html or htm
121
                        $extension = pathinfo(basename($document->path), PATHINFO_EXTENSION);
122
                        switch ($extension) {
123
                            case 'html':
124
                            case 'htm':
125
                                $contents = file_get_contents($doc_dir);
126
                                $contents = str_replace(
127
                                    $webEditorCss,
128
                                    '{{css_editor}}',
129
                                    $contents
130
                                );
131
                                file_put_contents($doc_dir, $contents);
132
                                break;
133
                        }
134
                    }
135
                } else {
136
                    @mkdir($backup_dir.$document->path, $perm_dirs, true);
137
                }
138
            }
139
        }
140
141
        // Copy all xapi resources to the temp-dir
142
        if (isset($course->resources[RESOURCE_XAPI_TOOL]) && is_array($course->resources[RESOURCE_XAPI_TOOL])) {
143
            foreach ($course->resources[RESOURCE_XAPI_TOOL] as $xapi) {
144
                $launchPath = str_replace(
145
                    api_get_path(WEB_COURSE_PATH).$course->info['path'].'/',
146
                    '',
147
                    dirname($xapi->params['launch_url'])
148
                );
149
                $xapiDir = dirname($backup_dir.'/'.$launchPath.'/');
150
                @mkdir($xapiDir, $perm_dirs, true);
151
                copyDirTo($course->path.$launchPath.'/', $backup_dir.$launchPath, false);
152
            }
153
        }
154
155
        // Copy all scorm documents to the temp-dir
156
        if (isset($course->resources[RESOURCE_SCORM]) && is_array($course->resources[RESOURCE_SCORM])) {
157
            foreach ($course->resources[RESOURCE_SCORM] as $document) {
158
                copyDirTo($course->path.$document->path, $backup_dir.$document->path, false);
159
            }
160
        }
161
162
        // Copy calendar attachments.
163
        if (isset($course->resources[RESOURCE_EVENT]) && is_array($course->resources[RESOURCE_EVENT])) {
164
            $doc_dir = dirname($backup_dir.'/upload/calendar/');
165
            @mkdir($doc_dir, $perm_dirs, true);
166
            copyDirTo($course->path.'upload/calendar/', $doc_dir, false);
167
        }
168
169
        // Copy Learning path author image.
170
        if (isset($course->resources[RESOURCE_LEARNPATH]) && is_array($course->resources[RESOURCE_LEARNPATH])) {
171
            $doc_dir = dirname($backup_dir.'/upload/learning_path/');
172
            @mkdir($doc_dir, $perm_dirs, true);
173
            copyDirTo($course->path.'upload/learning_path/', $doc_dir, false);
174
        }
175
176
        // Copy announcements attachments.
177
        if (isset($course->resources[RESOURCE_ANNOUNCEMENT]) && is_array($course->resources[RESOURCE_ANNOUNCEMENT])) {
178
            $doc_dir = dirname($backup_dir.'/upload/announcements/');
179
            @mkdir($doc_dir, $perm_dirs, true);
180
            copyDirTo($course->path.'upload/announcements/', $doc_dir, false);
181
        }
182
183
        // Copy work folders (only folders)
184
        if (isset($course->resources[RESOURCE_WORK]) && is_array($course->resources[RESOURCE_WORK])) {
185
            $doc_dir = $backup_dir.'work';
186
            @mkdir($doc_dir, $perm_dirs, true);
187
            copyDirWithoutFilesTo($course->path.'work/', $doc_dir);
188
        }
189
        if (isset($course->resources[RESOURCE_ASSET]) && is_array($course->resources[RESOURCE_ASSET])) {
190
            /** @var Asset $asset */
191
            foreach ($course->resources[RESOURCE_ASSET] as $asset) {
192
                $doc_dir = $backup_dir.$asset->path;
193
                @mkdir(dirname($doc_dir), $perm_dirs, true);
194
                $assetPath = $course->path.$asset->path;
195
196
                if (!file_exists($assetPath)) {
197
                    continue;
198
                }
199
200
                if (is_dir($course->path.$asset->path)) {
201
                    copyDirTo($course->path.$asset->path, $doc_dir, false);
202
                    continue;
203
                }
204
                copy($course->path.$asset->path, $doc_dir);
205
            }
206
        }
207
208
        // Zip the course-contents
209
        $zip = new \PclZip($zipFilePath);
210
        $zip->create($backup_dir, PCLZIP_OPT_REMOVE_PATH, $backup_dir);
0 ignored issues
show
Bug introduced by
The constant Chamilo\CourseBundle\Com...\PCLZIP_OPT_REMOVE_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
211
212
        // Remove the temp-dir.
213
        rmdirr($backup_dir);
214
215
        return $zipFileName;
216
    }
217
218
    /**
219
     * @param int $user_id
220
     *
221
     * @return array
222
     */
223
    public static function getAvailableBackups($user_id = null)
224
    {
225
        $backup_files = [];
226
        $dirname = self::getBackupDir();
227
228
        if (!file_exists($dirname)) {
229
            $dirname = self::createBackupDir();
230
        }
231
232
        if ($dir = opendir($dirname)) {
233
            while (($file = readdir($dir)) !== false) {
234
                $file_parts = explode('_', $file);
235
                if (count($file_parts) == 3) {
236
                    $owner_id = $file_parts[0];
237
                    $course_code = $file_parts[1];
238
                    $file_parts = explode('.', $file_parts[2]);
239
                    $date = $file_parts[0];
240
                    $ext = isset($file_parts[1]) ? $file_parts[1] : null;
241
                    if ($ext == 'zip' && ($user_id != null && $owner_id == $user_id || $user_id == null)) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $user_id of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing $user_id of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
242
                        $date =
243
                            substr($date, 0, 4).'-'.substr($date, 4, 2).'-'.
244
                            substr($date, 6, 2).' '.substr($date, 9, 2).':'.
245
                            substr($date, 11, 2).':'.substr($date, 13, 2);
246
                        $backup_files[] = [
247
                            'file' => $file,
248
                            'date' => $date,
249
                            'course_code' => $course_code,
250
                        ];
251
                    }
252
                }
253
            }
254
            closedir($dir);
255
        }
256
257
        return $backup_files;
258
    }
259
260
    /**
261
     * @param array|string $file Either $_FILES['...'] array or a tmp path string
262
     *
263
     * @return bool|string Returns the stored zip filename (not full path) or false on failure
264
     */
265
    public static function importUploadedFile($file)
266
    {
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);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

272
            /** @scrutinizer ignore-unhandled */ @mkdir($newDir, api_get_permissions_for_new_directories(), true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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;
288
        }
289
290
        if ($uploadErr !== 0 || $tmpPath === '') {
291
            return false;
292
        }
293
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);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

309
            /** @scrutinizer ignore-unhandled */ @unlink($destPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
310
            return false;
311
        }
312
313
        return $newFilename;
314
    }
315
316
    /**
317
     * Read a course-object from a zip-file.
318
     *
319
     * @param string $filename
320
     * @param bool   $delete   Delete the file after reading the course?
321
     *
322
     * @return course The course
323
     *
324
     * @todo Check if the archive is a correct Chamilo-export
325
     */
326
    public static function readCourse($filename, $delete = false)
327
    {
328
        self::cleanBackupDir();
329
        // Create a temp directory
330
        $tmp_dir_name = 'CourseArchiver_'.uniqid('');
331
        $unzip_dir = self::getBackupDir().$tmp_dir_name;
332
        $filePath = self::getBackupDir().$filename;
333
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
        }
345
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
352
        $zip = new \PclZip($unzip_dir.'/backup.zip');
353
354
        if (!@chdir($unzip_dir)) {
355
            error_log('[COURSE_ARCHIVER] readCourse: chdir failed unzip_dir="'.$unzip_dir.'"');
356
            return new Course();
357
        }
358
359
        // For course backups we must preserve original filenames so that
360
        // paths in course_info.dat still match the files in backup_path.
361
        $extractResult = $zip->extract(PCLZIP_OPT_TEMP_FILE_ON);
0 ignored issues
show
Bug introduced by
The constant Chamilo\CourseBundle\Com...PCLZIP_OPT_TEMP_FILE_ON was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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
        }
367
368
        // Remove the archive-file
369
        if ($delete) {
370
            @unlink($filePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

370
            /** @scrutinizer ignore-unhandled */ @unlink($filePath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
371
        }
372
373
        // Read the course
374
        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().'"');
382
            return new Course();
383
        }
384
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);
392
        @fclose($fp);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

392
        /** @scrutinizer ignore-unhandled */ @fclose($fp);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
393
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
401
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Course', 'Course');
402
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Announcement', 'Announcement');
403
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance', 'Attendance');
404
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent', 'CalendarEvent');
405
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyLearnpath', 'CourseCopyLearnpath');
406
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyTestCategory', 'CourseCopyTestCategory');
407
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseDescription', 'CourseDescription');
408
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseSession', 'CourseSession');
409
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Document', 'Document');
410
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Forum', 'Forum');
411
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumCategory', 'ForumCategory');
412
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumPost', 'ForumPost');
413
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumTopic', 'ForumTopic');
414
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Glossary', 'Glossary');
415
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup', 'GradeBookBackup');
416
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Link', 'Link');
417
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\LinkCategory', 'LinkCategory');
418
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Quiz', 'Quiz');
419
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestion', 'QuizQuestion');
420
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestionOption', 'QuizQuestionOption');
421
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ScormDocument', 'ScormDocument');
422
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Survey', 'Survey');
423
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyInvitation', 'SurveyInvitation');
424
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyQuestion', 'SurveyQuestion');
425
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Thematic', 'Thematic');
426
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ToolIntro', 'ToolIntro');
427
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki', 'Wiki');
428
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Work', 'Work');
429
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\XapiTool', 'XapiTool');
430
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
        }
441
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');
446
            return new Course();
447
        }
448
449
        // Ensure backup_path is always set when unserialize is successful
450
        $course->backup_path = $unzip_dir;
451
452
        return $course;
453
    }
454
}
455