2 /************************************************************************/
4 /************************************************************************/
5 /* Copyright (c) 2002-2010 */
6 /* Inclusive Design Institute */
8 /* This program is free software. You can redistribute it and/or */
9 /* modify it under the terms of the GNU General Public License */
10 /* as published by the Free Software Foundation. */
11 /************************************************************************/
14 require_once(AT_INCLUDE_PATH.'classes/zipfile.class.php');
15 require_once(AT_INCLUDE_PATH.'../mods/_core/file_manager/filemanager.inc.php'); //readfile_in_chunks folder
22 * Class for creating and managing course backups
24 * @author Joel Kronenberg
30 // number of backups in the backup dir
34 // the current course id
38 // where to store the backup
45 // the backup zipfile Object
48 // the timestamp for the zip files
52 // array of installed modules that support backups
58 function Backup(&$db, $course_id = 0) {
62 $this->setCourseID($course_id);
66 // should be used by the admin section
67 function setCourseID($course_id) {
68 $this->course_id = $course_id;
69 $this->backup_dir = AT_BACKUP_DIR . $course_id . DIRECTORY_SEPARATOR;
75 function generateFileName( ) {
76 global $system_courses;
77 $title = $system_courses[$this->course_id]['title'];
79 $title = str_replace(' ', '_', $title);
80 $title = str_replace('%', '', $title);
81 $title = str_replace('\'', '', $title);
82 $title = str_replace('"', '', $title);
83 $title = str_replace('`', '', $title);
85 $title .= '_' . date('d_M_y') . '.zip';
91 // NOTE: should the create() deal with saving it to disk as well? or should it be general to just create it, and not actually
92 // responsible for where to save it? (write a diff method to save it after)
93 function create($description) {
94 global $addslashes, $moduleFactory;
96 if ($this->getNumAvailable() >= AT_COURSE_BACKUPS) {
102 $zipfile = new zipfile();
104 $package_identifier = VERSION."\n\n\n".'Do not change the first line of this file it contains the ATutor version this backup was created with.';
105 $zipfile->add_file($package_identifier, 'atutor_backup_version', $timestamp);
107 // backup course properties. ONLY BANNER FOR NOW.
108 require_once(AT_INCLUDE_PATH . 'classes/CSVExport.class.php');
109 $CSVExport = new CSVExport();
112 $sql = 'SELECT banner
113 FROM '.TABLE_PREFIX.'courses
114 WHERE course_id='.$this->course_id;
115 $properties = $CSVExport->export($sql, $course_id);
116 $zipfile->add_file($properties, 'properties.csv', $now);
119 $modules = $moduleFactory->getModules(AT_MODULE_STATUS_ENABLED | AT_MODULE_STATUS_DISABLED);
120 $keys = array_keys($modules);
121 foreach($keys as $module_name) {
122 $module =& $modules[$module_name];
123 $module->backup($this->course_id, $zipfile);
127 $system_file_name = md5($timestamp);
129 if (!is_dir(AT_BACKUP_DIR)) {
130 @mkdir(AT_BACKUP_DIR);
133 if (!is_dir(AT_BACKUP_DIR . $this->course_id)) {
134 @mkdir(AT_BACKUP_DIR . $this->course_id);
137 $zipfile->write_file(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $system_file_name . '.zip');
139 $row['description'] = $addslashes($description);
140 $row['contents'] = addslashes(serialize($table_counters));
141 $row['system_file_name'] = $system_file_name;
142 $row['file_size'] = $zipfile->get_size();
143 $row['file_name'] = $this->generateFileName();
151 function upload($_FILES, $description) {
152 global $addslashes, $msg;
154 $ext = pathinfo($_FILES['file']['name']);
155 $ext = $ext['extension'];
157 if (!$_FILES['file']['name'] || !is_uploaded_file($_FILES['file']['tmp_name']) || ($ext != 'zip')) {
158 if ($_FILES['file']['error'] == 1) { // LEQ to UPLOAD_ERR_INI_SIZE
159 $errors = array('FILE_TOO_BIG', ini_get('upload_max_filesize'));
160 $msg->addError($errors);
162 $msg->addError('FILE_NOT_SELECTED');
166 if ($_FILES['file']['size'] == 0) {
167 $msg->addError('IMPORTFILE_EMPTY');
170 if($msg->containsErrors()) {
175 $row['description'] = $addslashes($description);
176 $row['system_file_name'] = md5(time());
177 $row['contents'] = '';
178 $row['file_size'] = $_FILES['file']['size'];
179 $row['file_name'] = $addslashes($_FILES['file']['name']);
181 if (!is_dir(AT_BACKUP_DIR)) {
182 @mkdir(AT_BACKUP_DIR);
185 if (!is_dir(AT_BACKUP_DIR . $this->course_id)) {
186 @mkdir(AT_BACKUP_DIR . $this->course_id);
189 $backup_path = AT_BACKUP_DIR . DIRECTORY_SEPARATOR . $this->course_id . DIRECTORY_SEPARATOR;
191 move_uploaded_file($_FILES['file']['tmp_name'], $backup_path . $row['system_file_name'].'.zip');
199 // adds a backup to the database
201 $sql = "INSERT INTO ".TABLE_PREFIX."backups VALUES (NULL, $this->course_id, NOW(), '$row[description]', '$row[file_size]', '$row[system_file_name]', '$row[file_name]', '$row[contents]')";
202 mysql_query($sql, $this->db);
206 // get number of backups
207 function getNumAvailable() {
208 // use $num_backups, if not set then do a COUNT(*) on the table
209 if (isset($this->num_backups)) {
210 return $this->num_backups;
213 $sql = "SELECT COUNT(*) AS cnt FROM ".TABLE_PREFIX."backups WHERE course_id=$this->course_id";
214 $result = mysql_query($sql, $this->db);
215 $row = mysql_fetch_assoc($result);
217 $this->num_backups = $row['cnt'];
222 // get list of backups
223 function getAvailableList() {
224 $backup_list = array();
226 $sql = "SELECT *, UNIX_TIMESTAMP(date) AS date_timestamp FROM ".TABLE_PREFIX."backups WHERE course_id=$this->course_id ORDER BY date DESC";
227 $result = mysql_query($sql, $this->db);
228 while ($row = mysql_fetch_assoc($result)) {
229 $backup_list[$row['backup_id']] = $row;
230 $backup_list[$row['backup_id']]['contents'] = unserialize($row['contents']);
233 $this->num_backups = count($backup_list);
239 function download($backup_id) { // or fetch()
240 $list = $this->getAvailableList($this->course_id);
241 if (!isset($list[$backup_id])) {
243 //debug('does not belong to us');
247 $my_backup = $list[$backup_id];
248 $file_name = $my_backup['file_name'];
250 header('Content-Type: application/zip');
251 header('Content-transfer-encoding: binary');
252 header('Content-Disposition: attachment; filename="'.htmlspecialchars($file_name).'"');
253 header('Expires: 0');
254 header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
255 header('Pragma: public');
256 header('Content-Length: '.$my_backup['file_size']);
258 // see the note in get.php about the use of x-Sendfile
260 header("Content-Encoding: none");
261 header('x-Sendfile: ' . AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
262 header('x-Sendfile: ', TRUE); // if we get here then it didn't work
264 readfile_in_chunks(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
269 function delete($backup_id) {
270 $list = $this->getAvailableList($this->course_id);
271 if (!isset($list[$backup_id])) {
273 //debug('does not belong to us');
276 $my_backup = $list[$backup_id];
278 // delete the backup file:
279 @unlink(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
281 // delete the row in the table:
282 $sql = "DELETE FROM ".TABLE_PREFIX."backups WHERE backup_id=$backup_id AND course_id=$this->course_id";
283 $result = mysql_query($sql, $this->db);
287 function edit($backup_id, $description) {
291 $backup_id = abs($backup_id);
292 $description = $addslashes($description);
294 // update description in the table:
295 $sql = "UPDATE ".TABLE_PREFIX."backups SET description='$description', date=date WHERE backup_id=$backup_id AND course_id=$this->course_id";
296 $result = mysql_query($sql, $this->db);
301 function getRow($backup_id, $course_id = 0) {
303 $backup_id = abs($backup_id);
304 $course_id = abs($course_id);
307 $sql = "SELECT *, UNIX_TIMESTAMP(date) AS date_timestamp FROM ".TABLE_PREFIX."backups WHERE backup_id=$backup_id AND course_id=$course_id";
309 $sql = "SELECT *, UNIX_TIMESTAMP(date) AS date_timestamp FROM ".TABLE_PREFIX."backups WHERE backup_id=$backup_id AND course_id=$this->course_id";
312 $result = mysql_query($sql, $this->db);
313 $row = mysql_fetch_assoc($result);
316 $row['contents'] = unserialize($row['contents']);
322 function translate_whitespace($input) {
323 $input = str_replace('\n', "\n", $input);
324 $input = str_replace('\r', "\r", $input);
325 $input = str_replace('\x00', "\0", $input);
331 function getVersion() {
332 if ((file_exists($this->import_dir.'atutor_backup_version')) && ($version = file($this->import_dir.'atutor_backup_version'))) {
333 return trim($version[0]);
340 function restore($material, $action, $backup_id, $from_course_id = 0) {
341 global $moduleFactory;
342 require_once(AT_INCLUDE_PATH.'classes/pclzip.lib.php');
343 require_once(AT_INCLUDE_PATH.'../mods/_core/file_manager/filemanager.inc.php');
345 if (!$from_course_id) {
346 $from_course_id = $this->course_id;
349 // 1. get backup row/information
350 $my_backup = $this->getRow($backup_id, $from_course_id);
352 @mkdir(AT_CONTENT_DIR . 'import/' . $this->course_id);
353 $this->import_dir = AT_CONTENT_DIR . 'import/' . $this->course_id . '/';
355 // 2. extract the backup
356 $archive = new PclZip(AT_BACKUP_DIR . $from_course_id. '/' . $my_backup['system_file_name']. '.zip');
357 if ($archive->extract( PCLZIP_OPT_PATH, $this->import_dir,
358 PCLZIP_CB_PRE_EXTRACT, 'preImportCallBack') == 0) {
359 die("Error : ".$archive->errorInfo(true));
362 // 3. get the course's max_quota. if backup is too big AND we want to import files then abort/return FALSE
363 /* get the course's max_quota */
364 // $this->getFilesSize();
366 // 4. figure out version number
367 $this->version = $this->getVersion();
368 if (!$this->version) {
369 clr_dir($this->import_dir);
371 $msg->addError('BACKUP_RESTORE');
372 header('Location: '.$_SERVER['PHP_SELF']);
374 //exit('version not found. backups < 1.3 are not supported.');
377 if (version_compare($this->version, VERSION, '>') == 1) {
378 clr_dir($this->import_dir);
381 $msg->addError('BACKUP_UNSUPPORTED_GREATER_VERSION');
382 header('Location: '.$_SERVER['PHP_SELF']);
385 if (version_compare($this_version, '1.5.3', '<')) {
386 if (file_exists($this->import_dir . 'resource_categories.csv')) {
387 @rename($this->import_dir . 'resource_categories.csv', $this->import_dir. 'links_categories.csv');
389 if (file_exists($this->import_dir . 'resource_links.csv')) {
390 @rename($this->import_dir . 'resource_links.csv', $this->import_dir. 'links.csv');
394 // 5. if override is set then delete the content
395 if ($action == 'overwrite') {
396 require_once(AT_INCLUDE_PATH.'../mods/_core/properties/lib/delete_course.inc.php');
397 delete_course($this->course_id, $material);
398 $_SESSION['s_cid'] = 0;
399 } // else: appending content
401 if ($material === TRUE) {
402 // restore the entire backup (used when creating a new course)
403 $module_list = $moduleFactory->getModules(AT_MODULE_ENABLED | AT_MODULE_CORE);
404 $_POST['material'] = $module_list;
406 foreach ($_POST['material'] as $module_name => $garbage) {
407 // restore course properties, ONLY BANNER FOR NOW.
408 if ($module_name == 'properties' && file_exists($this->import_dir . "properties.csv"))
412 $fp = @fopen($this->import_dir . "properties.csv", 'rb');
414 if (($row = @fgetcsv($fp, 70000)) !== false)
416 //hack for http://www.atutor.ca/atutor/mantis/view.php?id=3839
417 $row[0] = preg_replace('/\\\\r\\\\n/', "\r\n", $row[0]);
419 $sql = "UPDATE ".TABLE_PREFIX."courses
420 SET banner = '". mysql_real_escape_string($row[0]). "'
421 WHERE course_id = ".$this->course_id;
422 $result = mysql_query($sql,$db) or die(mysql_error());
427 $module = $moduleFactory->getModule($module_name);
428 $module->restore($this->course_id, $this->version, $this->import_dir);
430 clr_dir($this->import_dir);
435 function restore_files() {
436 $sql = "SELECT max_quota FROM ".TABLE_PREFIX."courses WHERE course_id=$this->course_id";
437 $result = mysql_query($sql, $this->db);
438 $row = mysql_fetch_assoc($result);
440 if ($row['max_quota'] != AT_COURSESIZE_UNLIMITED) {
441 global $MaxCourseSize, $MaxCourseFloat;
443 if ($row['max_quota'] == AT_COURSESIZE_DEFAULT) {
444 $row['max_quota'] = $MaxCourseSize;
447 $totalBytes = dirsize($this->import_dir . 'content/');
449 $course_total = dirsize(AT_CONTENT_DIR . $this->course_id . '/');
451 $total_after = $row['max_quota'] - $course_total - $totalBytes + $MaxCourseFloat;
453 if ($total_after < 0) {
454 //debug('not enough space. delete everything');
455 // remove the content dir, since there's no space for it
456 clr_dir($this->import_dir);
461 copys($this->import_dir.'content/', AT_CONTENT_DIR . $this->course_id);