Issues (2037)

Component/CourseCopy/CourseArchiver.php (12 issues)

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);
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 $file
262
     *
263
     * @return bool|string
264
     */
265
    public static function importUploadedFile($file)
266
    {
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);
272
        }
273
        if (is_dir($new_dir) && is_writable($new_dir)) {
274
            move_uploaded_file($file, $new_dir.$new_filename);
275
276
            return $new_filename;
277
        }
278
279
        return false;
280
    }
281
282
    /**
283
     * Read a course-object from a zip-file.
284
     *
285
     * @param string $filename
286
     * @param bool   $delete   Delete the file after reading the course?
287
     *
288
     * @return course The course
289
     *
290
     * @todo Check if the archive is a correct Chamilo-export
291
     */
292
    public static function readCourse($filename, $delete = false)
293
    {
294
        self::cleanBackupDir();
295
        // Create a temp directory
296
        $tmp_dir_name = 'CourseArchiver_'.uniqid('');
297
        $unzip_dir = self::getBackupDir().$tmp_dir_name;
298
        $filePath = self::getBackupDir().$filename;
299
300
        @mkdir($unzip_dir, 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

300
        /** @scrutinizer ignore-unhandled */ @mkdir($unzip_dir, 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...
301
        @copy(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for copy(). 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

301
        /** @scrutinizer ignore-unhandled */ @copy(

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...
302
            $filePath,
303
            $unzip_dir.'/backup.zip'
304
        );
305
306
        // unzip the archive
307
        $zip = new \PclZip($unzip_dir.'/backup.zip');
308
        @chdir($unzip_dir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chdir(). 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

308
        /** @scrutinizer ignore-unhandled */ @chdir($unzip_dir);

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...
309
310
        $zip->extract(
311
            PCLZIP_OPT_TEMP_FILE_ON,
312
            PCLZIP_CB_PRE_EXTRACT,
313
            'clean_up_files_in_zip'
314
        );
315
316
        // remove the archive-file
317
        if ($delete) {
318
            @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

318
            /** @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...
319
        }
320
321
        // read the course
322
        if (!is_file('course_info.dat')) {
323
            return new Course();
324
        }
325
326
        $fp = @fopen('course_info.dat', "r");
327
        $contents = @fread($fp, filesize('course_info.dat'));
328
        @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

328
        /** @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...
329
330
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Course', 'Course');
331
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Announcement', 'Announcement');
332
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance', 'Attendance');
333
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent', 'CalendarEvent');
334
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyLearnpath', 'CourseCopyLearnpath');
335
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyTestCategory', 'CourseCopyTestCategory');
336
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseDescription', 'CourseDescription');
337
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseSession', 'CourseSession');
338
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Document', 'Document');
339
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Forum', 'Forum');
340
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumCategory', 'ForumCategory');
341
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumPost', 'ForumPost');
342
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumTopic', 'ForumTopic');
343
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Glossary', 'Glossary');
344
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup', 'GradeBookBackup');
345
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Link', 'Link');
346
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\LinkCategory', 'LinkCategory');
347
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Quiz', 'Quiz');
348
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestion', 'QuizQuestion');
349
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestionOption', 'QuizQuestionOption');
350
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ScormDocument', 'ScormDocument');
351
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Survey', 'Survey');
352
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyInvitation', 'SurveyInvitation');
353
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyQuestion', 'SurveyQuestion');
354
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Thematic', 'Thematic');
355
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\ToolIntro', 'ToolIntro');
356
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki', 'Wiki');
357
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\Work', 'Work');
358
        class_alias('Chamilo\CourseBundle\Component\CourseCopy\Resources\XapiTool', 'XapiTool');
359
360
        /** @var Course $course */
361
        $course = \UnserializeApi::unserialize('course', base64_decode($contents));
362
363
        if (!in_array(
364
            get_class($course),
365
            ['Course', 'Chamilo\CourseBundle\Component\CourseCopy\Course']
366
        )
367
        ) {
368
            return new Course();
369
        }
370
371
        $course->backup_path = $unzip_dir;
372
373
        return $course;
374
    }
375
}
376