Passed
Push — master ( 9df631...0f56b8 )
by
unknown
10:44
created

CourseArchiver   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 459
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 229
dl 0
loc 459
rs 2
c 0
b 0
f 0
wmc 87

10 Methods

Rating   Name   Duplication   Size   Complexity  
F createBackup() 0 136 32
A getBackupDir() 0 3 1
A createBackupDir() 0 8 1
B cleanBackupDir() 0 14 8
B getAvailableBackups() 0 35 10
A importUploadedFile() 0 15 4
C readCourse() 0 80 14
A coerceNumericStringsInSerialized() 0 37 2
C normalizeIds() 0 25 12
A registerLegacyAliases() 0 36 3

How to fix   Complexity   

Complex Class

Complex classes like CourseArchiver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CourseArchiver, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
namespace Chamilo\CourseBundle\Component\CourseCopy;
6
7
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Asset;
8
use Chamilo\CourseBundle\Component\CourseCopy\Resources\Document;
9
use DateTime;
10
use PclZip;
11
use Symfony\Component\Filesystem\Filesystem;
12
use ZipArchive;
13
14
/**
15
 * Some functions to write a course-object to a zip-file and to read a course-
16
 * object from such a zip-file.
17
 *
18
 * @author Bart Mollet <[email protected]>
19
 *
20
 * @todo Use archive-folder of Chamilo?
21
 */
22
class CourseArchiver
23
{
24
    /**
25
     * @return string
26
     */
27
    public static function getBackupDir()
28
    {
29
        return api_get_path(SYS_ARCHIVE_PATH).'course_backups/';
30
    }
31
32
    /**
33
     * @return string
34
     */
35
    public static function createBackupDir()
36
    {
37
        $perms = api_get_permissions_for_new_directories();
38
        $dir = self::getBackupDir();
39
        $fs = new Filesystem();
40
        $fs->mkdir($dir, $perms);
41
42
        return $dir;
43
    }
44
45
    /**
46
     * Delete old temp-dirs.
47
     */
48
    public static function cleanBackupDir()
49
    {
50
        $dir = self::getBackupDir();
51
        if (is_dir($dir)) {
52
            if ($handle = @opendir($dir)) {
53
                while (false !== ($file = readdir($handle))) {
54
                    if ('.' != $file && '..' != $file &&
55
                        0 === strpos($file, 'CourseArchiver_') &&
56
                        is_dir($dir.'/'.$file)
57
                    ) {
58
                        rmdirr($dir.'/'.$file);
59
                    }
60
                }
61
                closedir($handle);
62
            }
63
        }
64
    }
65
66
    /**
67
     * Write a course and all its resources to a zip-file.
68
     *
69
     * @return string A pointer to the zip-file
70
     */
71
    public static function createBackup($course)
72
    {
73
        self::cleanBackupDir();
74
        self::createBackupDir();
75
76
        $perm_dirs = api_get_permissions_for_new_directories();
77
        $backupDirectory = self::getBackupDir();
78
79
        // Create a temp directory
80
        $backup_dir = $backupDirectory.'CourseArchiver_'.api_get_unique_id().'/';
81
82
        // All course-information will be stored in course_info.dat
83
        $course_info_file = $backup_dir.'course_info.dat';
84
85
        $user = api_get_user_info();
86
        $date = new DateTime(api_get_local_time());
87
        $zipFileName = $user['user_id'].'_'.$course->code.'_'.$date->format('Ymd-His').'.zip';
88
        $zipFilePath = $backupDirectory.$zipFileName;
89
90
        $php_errormsg = '';
91
        $res = @mkdir($backup_dir, $perm_dirs);
92
        if (false === $res) {
93
            //TODO set and handle an error message telling the user to review the permissions on the archive directory
94
            error_log(__FILE__.' line '.__LINE__.': '.(false != ini_get('track_errors') ? $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...
95
        }
96
        // Write the course-object to the file
97
        $fp = @fopen($course_info_file, 'w');
98
        if (false === $fp) {
99
            error_log(__FILE__.' line '.__LINE__.': '.(false != ini_get('track_errors') ? $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...
100
        }
101
102
        $res = @fwrite($fp, base64_encode(serialize($course)));
103
        if (false === $res) {
104
            error_log(__FILE__.' line '.__LINE__.': '.(false != ini_get('track_errors') ? $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...
105
        }
106
107
        $res = @fclose($fp);
108
        if (false === $res) {
109
            error_log(__FILE__.' line '.__LINE__.': '.(false != ini_get('track_errors') ? $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...
110
        }
111
112
        // Copy all documents to the temp-dir
113
        if (isset($course->resources[RESOURCE_DOCUMENT]) && is_array($course->resources[RESOURCE_DOCUMENT])) {
114
            $webEditorCss = api_get_path(WEB_CSS_PATH).'editor.css';
115
            /** @var Document $document */
116
            foreach ($course->resources[RESOURCE_DOCUMENT] as $document) {
117
                if ('document' === $document->file_type) {
118
                    $doc_dir = $backup_dir.$document->path;
119
                    @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

119
                    /** @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...
120
                    if (file_exists($course->path.$document->path)) {
121
                        copy($course->path.$document->path, $doc_dir);
122
                        // Check if is html or htm
123
                        $extension = pathinfo(basename($document->path), PATHINFO_EXTENSION);
124
                        switch ($extension) {
125
                            case 'html':
126
                            case 'htm':
127
                                $contents = file_get_contents($doc_dir);
128
                                $contents = str_replace(
129
                                    $webEditorCss,
130
                                    '{{css_editor}}',
131
                                    $contents
132
                                );
133
                                file_put_contents($doc_dir, $contents);
134
135
                                break;
136
                        }
137
                    }
138
                } else {
139
                    @mkdir($backup_dir.$document->path, $perm_dirs, true);
140
                }
141
            }
142
        }
143
144
        // Copy all scorm documents to the temp-dir
145
        if (isset($course->resources[RESOURCE_SCORM]) && is_array($course->resources[RESOURCE_SCORM])) {
146
            foreach ($course->resources[RESOURCE_SCORM] as $document) {
147
                copyDirTo($course->path.$document->path, $backup_dir.$document->path, false);
148
            }
149
        }
150
151
        // Copy calendar attachments.
152
        if (isset($course->resources[RESOURCE_EVENT]) && is_array($course->resources[RESOURCE_EVENT])) {
153
            $doc_dir = dirname($backup_dir.'/upload/calendar/');
154
            @mkdir($doc_dir, $perm_dirs, true);
155
            copyDirTo($course->path.'upload/calendar/', $doc_dir, false);
156
        }
157
158
        // Copy Learning path author image.
159
        if (isset($course->resources[RESOURCE_LEARNPATH]) && is_array($course->resources[RESOURCE_LEARNPATH])) {
160
            $doc_dir = dirname($backup_dir.'/upload/learning_path/');
161
            @mkdir($doc_dir, $perm_dirs, true);
162
            copyDirTo($course->path.'upload/learning_path/', $doc_dir, false);
163
        }
164
165
        // Copy announcements attachments.
166
        if (isset($course->resources[RESOURCE_ANNOUNCEMENT]) && is_array($course->resources[RESOURCE_ANNOUNCEMENT])) {
167
            $doc_dir = dirname($backup_dir.'/upload/announcements/');
168
            @mkdir($doc_dir, $perm_dirs, true);
169
            copyDirTo($course->path.'upload/announcements/', $doc_dir, false);
170
        }
171
172
        // Copy work folders (only folders)
173
        if (isset($course->resources[RESOURCE_WORK]) && is_array($course->resources[RESOURCE_WORK])) {
174
            $doc_dir = $backup_dir.'work';
175
            @mkdir($doc_dir, $perm_dirs, true);
176
            copyDirWithoutFilesTo($course->path.'work/', $doc_dir);
0 ignored issues
show
Bug introduced by
The function copyDirWithoutFilesTo was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

176
            /** @scrutinizer ignore-call */ 
177
            copyDirWithoutFilesTo($course->path.'work/', $doc_dir);
Loading history...
177
        }
178
179
        if (isset($course->resources[RESOURCE_ASSET]) && is_array($course->resources[RESOURCE_ASSET])) {
180
            /** @var Asset $asset */
181
            foreach ($course->resources[RESOURCE_ASSET] as $asset) {
182
                $doc_dir = $backup_dir.$asset->path;
183
                @mkdir(dirname($doc_dir), $perm_dirs, true);
184
                $assetPath = $course->path.$asset->path;
185
186
                if (!file_exists($assetPath)) {
187
                    continue;
188
                }
189
190
                if (is_dir($course->path.$asset->path)) {
191
                    copyDirTo($course->path.$asset->path, $doc_dir, false);
192
193
                    continue;
194
                }
195
                copy($course->path.$asset->path, $doc_dir);
196
            }
197
        }
198
199
        // Zip the course-contents
200
        $zip = new PclZip($zipFilePath);
201
        $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...
202
203
        // Remove the temp-dir.
204
        rmdirr($backup_dir);
205
206
        return $zipFileName;
207
    }
208
209
    /**
210
     * @param int $user_id
211
     *
212
     * @return array
213
     */
214
    public static function getAvailableBackups($user_id = null)
215
    {
216
        $backup_files = [];
217
        $dirname = self::getBackupDir();
218
219
        if (!file_exists($dirname)) {
220
            $dirname = self::createBackupDir();
221
        }
222
223
        if ($dir = opendir($dirname)) {
224
            while (false !== ($file = readdir($dir))) {
225
                $file_parts = explode('_', $file);
226
                if (3 == count($file_parts)) {
227
                    $owner_id = $file_parts[0];
228
                    $course_code = $file_parts[1];
229
                    $file_parts = explode('.', $file_parts[2]);
230
                    $date = $file_parts[0];
231
                    $ext = isset($file_parts[1]) ? $file_parts[1] : null;
232
                    if ('zip' == $ext && (null != $user_id && $owner_id == $user_id || null == $user_id)) {
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...
233
                        $date =
234
                            substr($date, 0, 4).'-'.substr($date, 4, 2).'-'.
235
                            substr($date, 6, 2).' '.substr($date, 9, 2).':'.
236
                            substr($date, 11, 2).':'.substr($date, 13, 2);
237
                        $backup_files[] = [
238
                            'file' => $file,
239
                            'date' => $date,
240
                            'course_code' => $course_code,
241
                        ];
242
                    }
243
                }
244
            }
245
            closedir($dir);
246
        }
247
248
        return $backup_files;
249
    }
250
251
    /**
252
     * @param array $file
253
     *
254
     * @return bool|string
255
     */
256
    public static function importUploadedFile($file)
257
    {
258
        $new_filename = uniqid('import_file', true).'.zip';
259
        $new_dir = self::getBackupDir();
260
        if (!is_dir($new_dir)) {
261
            $fs = new Filesystem();
262
            $fs->mkdir($new_dir);
263
        }
264
        if (is_dir($new_dir) && is_writable($new_dir)) {
265
            move_uploaded_file($file, $new_dir.$new_filename);
266
267
            return $new_filename;
268
        }
269
270
        return false;
271
    }
272
273
    /**
274
     * Read a legacy course backup (.zip) and return a Course object.
275
     * - Extracts the zip into a temp dir.
276
     * - Finds and decodes course_info.dat (base64 + serialize).
277
     * - Registers legacy aliases/stubs BEFORE unserialize (critical).
278
     * - Normalizes common identifier fields to int after unserialize.
279
     */
280
    public static function readCourse(string $filename, bool $delete = false): false|Course
281
    {
282
        // Clean temp backup dirs and ensure backup dir exists
283
        self::cleanBackupDir();
284
        self::createBackupDir();
285
286
        $backupDir = rtrim(self::getBackupDir(), '/').'/';
287
        $zipPath   = $backupDir.$filename;
288
289
        if (!is_file($zipPath)) {
290
            throw new \RuntimeException('Backup file not found: '.$filename);
291
        }
292
293
        // 1) Extract zip into a temp directory
294
        $tmp = $backupDir.'CourseArchiver_'.uniqid('', true).'/';
295
        (new Filesystem())->mkdir($tmp);
296
297
        $zip = new ZipArchive();
298
        if (true !== $zip->open($zipPath)) {
299
            throw new \RuntimeException('Cannot open zip: '.$filename);
300
        }
301
        if (!$zip->extractTo($tmp)) {
302
            $zip->close();
303
            throw new \RuntimeException('Cannot extract zip: '.$filename);
304
        }
305
        $zip->close();
306
307
        // 2) Read and decode course_info.dat (base64 + serialize)
308
        $courseInfoDat = $tmp.'course_info.dat';
309
        if (!is_file($courseInfoDat)) {
310
            // Fallback: search nested locations
311
            $rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tmp));
312
            foreach ($rii as $f) {
313
                if ($f->isFile() && $f->getFilename() === 'course_info.dat') {
314
                    $courseInfoDat = $f->getPathname();
315
                    break;
316
                }
317
            }
318
            if (!is_file($courseInfoDat)) {
319
                throw new \RuntimeException('course_info.dat not found in backup');
320
            }
321
        }
322
323
        $raw = file_get_contents($courseInfoDat);
324
        $payload = base64_decode($raw, true);
325
        if ($payload === false) {
326
            throw new \RuntimeException('course_info.dat is not valid base64');
327
        }
328
329
        // 3) Coerce numeric-string identifiers to integers *before* unserialize
330
        //    This prevents "Cannot assign string to property ... of type int"
331
        //    on typed properties (handles public/protected/private names).
332
        $payload = self::coerceNumericStringsInSerialized($payload);
333
334
        // 4) Register legacy aliases BEFORE unserialize (critical for v1 backups)
335
        self::registerLegacyAliases();
336
337
        // 5) Unserialize using UnserializeApi if present (v1-compatible)
338
        if (class_exists('UnserializeApi')) {
339
            /** @var Course $course */
340
            $course = \UnserializeApi::unserialize('course', $payload);
341
        } else {
342
            /** @var Course|false $course */
343
            $course = @unserialize($payload, ['allowed_classes' => true]);
344
        }
345
346
        if (!is_object($course)) {
347
            throw new \RuntimeException('Could not unserialize legacy course');
348
        }
349
350
        // 6) Normalize common numeric identifiers after unserialize (extra safety)
351
        self::normalizeIds($course);
352
353
        // 7) Optionally delete uploaded file (compat with v1)
354
        if ($delete && is_file($zipPath)) {
355
            @unlink($zipPath);
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

355
            /** @scrutinizer ignore-unhandled */ @unlink($zipPath);

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...
356
        }
357
358
        // Keep temp dir until restore phase if files are needed later (compat with v1)
359
        return $course;
360
    }
361
362
    /**
363
     * Convert selected numeric-string fields to integers inside the serialized payload
364
     * to avoid "Cannot assign string to property ... of type int" on typed properties.
365
     *
366
     * It handles public, protected ("\0*\0key") and private ("\0Class\0key") property names.
367
     * We only coerce known identifier keys to keep it safe.
368
     */
369
    private static function coerceNumericStringsInSerialized(string $ser): string
370
    {
371
        // Identifier keys that must be integers
372
        $keys = [
373
            'id','iid','c_id','parent_id','thematic_id','attendance_id',
374
            'room_id','display_order','session_id','category_id',
375
        ];
376
377
        /**
378
         * Build a pattern that matches any of these name encodings:
379
         *  - public:   "id"
380
         *  - protected:"\0*\0id"
381
         *  - private:  "\0SomeClass\0id"
382
         *
383
         * We don't touch the key itself (so its s:N length stays valid).
384
         * We only replace the *value* part: s:M:"123"  =>  i:123
385
         */
386
        $alternatives = [];
387
        foreach ($keys as $k) {
388
            // public
389
            $alternatives[] = preg_quote($k, '/');
390
            // protected
391
            $alternatives[] = "\x00\*\x00".preg_quote($k, '/');
392
            // private (class-specific; we accept any class name between NULs)
393
            $alternatives[] = "\x00[^\x00]+\x00".preg_quote($k, '/');
394
        }
395
        $nameAlt = '(?:'.implode('|', $alternatives).')';
396
397
        // Full pattern:
398
        // (s:\d+:"<any of the above forms>";) s:\d+:"(<digits>)";
399
        // Note: we *must not* use /u because of NUL bytes; keep binary-safe regex.
400
        $pattern = '/(s:\d+:"'.$nameAlt.'";)s:\d+:"(\d+)";/s';
401
402
        return preg_replace_callback(
403
            $pattern,
404
            static fn($m) => $m[1].'i:'.$m[2].';',
405
            $ser
406
        );
407
    }
408
409
410
    /**
411
     * Recursively cast common identifier fields to int after unserialize.
412
     * Safe to call on arrays/objects/stdClass/legacy resource objects.
413
     */
414
    private static function normalizeIds(mixed &$node): void
415
    {
416
        $castKeys = [
417
            'id','iid','c_id','parent_id','thematic_id','attendance_id',
418
            'room_id','display_order','session_id','category_id',
419
        ];
420
421
        if (is_array($node)) {
422
            foreach ($node as $k => &$v) {
423
                if (is_string($k) && in_array($k, $castKeys, true) && (is_string($v) || is_numeric($v))) {
424
                    $v = (int) $v;
425
                } else {
426
                    self::normalizeIds($v);
427
                }
428
            }
429
            return;
430
        }
431
432
        if (is_object($node)) {
433
            foreach (get_object_vars($node) as $k => $v) {
434
                if (in_array($k, $castKeys, true) && (is_string($v) || is_numeric($v))) {
435
                    $node->$k = (int) $v;
436
                    continue;
437
                }
438
                self::normalizeIds($node->$k);
439
            }
440
        }
441
    }
442
443
444
    /** Keep the old alias map so unserialize works exactly like v1 */
445
    private static function registerLegacyAliases(): void
446
    {
447
        $aliases = [
448
            'Chamilo\CourseBundle\Component\CourseCopy\Course'                 => 'Course',
449
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Announcement' => 'Announcement',
450
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance'   => 'Attendance',
451
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent'=> 'CalendarEvent',
452
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyLearnpath' => 'CourseCopyLearnpath',
453
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyTestCategory' => 'CourseCopyTestCategory',
454
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseDescription' => 'CourseDescription',
455
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseSession' => 'CourseSession',
456
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Document'     => 'Document',
457
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Forum'        => 'Forum',
458
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumCategory'=> 'ForumCategory',
459
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumPost'    => 'ForumPost',
460
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumTopic'   => 'ForumTopic',
461
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Glossary'     => 'Glossary',
462
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup' => 'GradeBookBackup',
463
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Link'         => 'Link',
464
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\LinkCategory' => 'LinkCategory',
465
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Quiz'         => 'Quiz',
466
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestion' => 'QuizQuestion',
467
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestionOption' => 'QuizQuestionOption',
468
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\ScormDocument'=> 'ScormDocument',
469
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Survey'       => 'Survey',
470
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyInvitation' => 'SurveyInvitation',
471
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyQuestion'=> 'SurveyQuestion',
472
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Thematic'     => 'Thematic',
473
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\ToolIntro'    => 'ToolIntro',
474
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki'         => 'Wiki',
475
            'Chamilo\CourseBundle\Component\CourseCopy\Resources\Work'         => 'Work',
476
        ];
477
478
        foreach ($aliases as $fqcn => $alias) {
479
            if (!class_exists($alias)) {
480
                class_alias($fqcn, $alias);
481
            }
482
        }
483
    }
484
}
485