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 /************************************************************************/
12 // $Id: Backup.class.php 8901 2009-11-11 19:10:19Z cindy $
14 require_once(AT_INCLUDE_PATH.'classes/zipfile.class.php');
21 * Class for creating and managing course backups
23 * @author Joel Kronenberg
29 // number of backups in the backup dir
33 // the current course id
37 // where to store the backup
44 // the backup zipfile Object
47 // the timestamp for the zip files
51 // array of installed modules that support backups
57 function Backup(&$db, $course_id = 0) {
61 $this->setCourseID($course_id);
65 // should be used by the admin section
66 function setCourseID($course_id) {
67 $this->course_id = $course_id;
68 $this->backup_dir = AT_BACKUP_DIR . $course_id . DIRECTORY_SEPARATOR;
74 function generateFileName( ) {
75 global $system_courses;
76 $title = $system_courses[$this->course_id]['title'];
78 $title = str_replace(' ', '_', $title);
79 $title = str_replace('%', '', $title);
80 $title = str_replace('\'', '', $title);
81 $title = str_replace('"', '', $title);
82 $title = str_replace('`', '', $title);
84 $title .= '_' . date('d_M_y') . '.zip';
90 // NOTE: should the create() deal with saving it to disk as well? or should it be general to just create it, and not actually
91 // responsible for where to save it? (write a diff method to save it after)
92 function create($description) {
93 global $addslashes, $moduleFactory;
95 if ($this->getNumAvailable() >= AT_COURSE_BACKUPS) {
101 $zipfile = new zipfile();
103 $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.';
104 $zipfile->add_file($package_identifier, 'atutor_backup_version', $timestamp);
106 // backup course properties. ONLY BANNER FOR NOW.
107 require_once(AT_INCLUDE_PATH . 'classes/CSVExport.class.php');
108 $CSVExport = new CSVExport();
111 $sql = 'SELECT banner
112 FROM '.TABLE_PREFIX.'courses
113 WHERE course_id='.$this->course_id;
114 $properties = $CSVExport->export($sql, $course_id);
115 $zipfile->add_file($properties, 'properties.csv', $now);
118 $modules = $moduleFactory->getModules(AT_MODULE_STATUS_ENABLED | AT_MODULE_STATUS_DISABLED);
119 $keys = array_keys($modules);
120 foreach($keys as $module_name) {
121 $module =& $modules[$module_name];
122 $module->backup($this->course_id, $zipfile);
126 $system_file_name = md5($timestamp);
128 if (!is_dir(AT_BACKUP_DIR)) {
129 @mkdir(AT_BACKUP_DIR);
132 if (!is_dir(AT_BACKUP_DIR . $this->course_id)) {
133 @mkdir(AT_BACKUP_DIR . $this->course_id);
136 $zipfile->write_file(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $system_file_name . '.zip');
138 $row['description'] = $addslashes($description);
139 $row['contents'] = addslashes(serialize($table_counters));
140 $row['system_file_name'] = $system_file_name;
141 $row['file_size'] = $zipfile->get_size();
142 $row['file_name'] = $this->generateFileName();
150 function upload($_FILES, $description) {
151 global $addslashes, $msg;
153 $ext = pathinfo($_FILES['file']['name']);
154 $ext = $ext['extension'];
156 if (!$_FILES['file']['name'] || !is_uploaded_file($_FILES['file']['tmp_name']) || ($ext != 'zip')) {
157 if ($_FILES['file']['error'] == 1) { // LEQ to UPLOAD_ERR_INI_SIZE
158 $errors = array('FILE_TOO_BIG', ini_get('upload_max_filesize'));
159 $msg->addError($errors);
161 $msg->addError('FILE_NOT_SELECTED');
165 if ($_FILES['file']['size'] == 0) {
166 $msg->addError('IMPORTFILE_EMPTY');
169 if($msg->containsErrors()) {
174 $row['description'] = $addslashes($description);
175 $row['system_file_name'] = md5(time());
176 $row['contents'] = '';
177 $row['file_size'] = $_FILES['file']['size'];
178 $row['file_name'] = $addslashes($_FILES['file']['name']);
180 if (!is_dir(AT_BACKUP_DIR)) {
181 @mkdir(AT_BACKUP_DIR);
184 if (!is_dir(AT_BACKUP_DIR . $this->course_id)) {
185 @mkdir(AT_BACKUP_DIR . $this->course_id);
188 $backup_path = AT_BACKUP_DIR . DIRECTORY_SEPARATOR . $this->course_id . DIRECTORY_SEPARATOR;
190 move_uploaded_file($_FILES['file']['tmp_name'], $backup_path . $row['system_file_name'].'.zip');
198 // adds a backup to the database
200 $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]')";
201 mysql_query($sql, $this->db);
205 // get number of backups
206 function getNumAvailable() {
207 // use $num_backups, if not set then do a COUNT(*) on the table
208 if (isset($this->num_backups)) {
209 return $this->num_backups;
212 $sql = "SELECT COUNT(*) AS cnt FROM ".TABLE_PREFIX."backups WHERE course_id=$this->course_id";
213 $result = mysql_query($sql, $this->db);
214 $row = mysql_fetch_assoc($result);
216 $this->num_backups = $row['cnt'];
221 // get list of backups
222 function getAvailableList() {
223 $backup_list = array();
225 $sql = "SELECT *, UNIX_TIMESTAMP(date) AS date_timestamp FROM ".TABLE_PREFIX."backups WHERE course_id=$this->course_id ORDER BY date DESC";
226 $result = mysql_query($sql, $this->db);
227 while ($row = mysql_fetch_assoc($result)) {
228 $backup_list[$row['backup_id']] = $row;
229 $backup_list[$row['backup_id']]['contents'] = unserialize($row['contents']);
232 $this->num_backups = count($backup_list);
238 function download($backup_id) { // or fetch()
239 $list = $this->getAvailableList($this->course_id);
240 if (!isset($list[$backup_id])) {
242 //debug('does not belong to us');
246 $my_backup = $list[$backup_id];
247 $file_name = $my_backup['file_name'];
249 header('Content-Type: application/zip');
250 header('Content-transfer-encoding: binary');
251 header('Content-Disposition: attachment; filename="'.htmlspecialchars($file_name).'"');
252 header('Expires: 0');
253 header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
254 header('Pragma: public');
255 header('Content-Length: '.$my_backup['file_size']);
257 // see the note in get.php about the use of x-Sendfile
259 header("Content-Encoding: none");
260 header('x-Sendfile: ' . AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
261 header('x-Sendfile: ', TRUE); // if we get here then it didn't work
263 readfile(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
268 function delete($backup_id) {
269 $list = $this->getAvailableList($this->course_id);
270 if (!isset($list[$backup_id])) {
272 //debug('does not belong to us');
275 $my_backup = $list[$backup_id];
277 // delete the backup file:
278 @unlink(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
280 // delete the row in the table:
281 $sql = "DELETE FROM ".TABLE_PREFIX."backups WHERE backup_id=$backup_id AND course_id=$this->course_id";
282 $result = mysql_query($sql, $this->db);
286 function edit($backup_id, $description) {
290 $backup_id = abs($backup_id);
291 $description = $addslashes($description);
293 // update description in the table:
294 $sql = "UPDATE ".TABLE_PREFIX."backups SET description='$description', date=date WHERE backup_id=$backup_id AND course_id=$this->course_id";
295 $result = mysql_query($sql, $this->db);
300 function getRow($backup_id, $course_id = 0) {
302 $backup_id = abs($backup_id);
303 $course_id = abs($course_id);
306 $sql = "SELECT *, UNIX_TIMESTAMP(date) AS date_timestamp FROM ".TABLE_PREFIX."backups WHERE backup_id=$backup_id AND course_id=$course_id";
308 $sql = "SELECT *, UNIX_TIMESTAMP(date) AS date_timestamp FROM ".TABLE_PREFIX."backups WHERE backup_id=$backup_id AND course_id=$this->course_id";
311 $result = mysql_query($sql, $this->db);
312 $row = mysql_fetch_assoc($result);
315 $row['contents'] = unserialize($row['contents']);
321 function translate_whitespace($input) {
322 $input = str_replace('\n', "\n", $input);
323 $input = str_replace('\r', "\r", $input);
324 $input = str_replace('\x00', "\0", $input);
330 function getVersion() {
331 if ((file_exists($this->import_dir.'atutor_backup_version')) && ($version = file($this->import_dir.'atutor_backup_version'))) {
332 return trim($version[0]);
339 function restore($material, $action, $backup_id, $from_course_id = 0) {
340 global $moduleFactory;
341 require_once(AT_INCLUDE_PATH.'classes/pclzip.lib.php');
342 require_once(AT_INCLUDE_PATH.'../mods/_core/file_manager/filemanager.inc.php');
344 if (!$from_course_id) {
345 $from_course_id = $this->course_id;
348 // 1. get backup row/information
349 $my_backup = $this->getRow($backup_id, $from_course_id);
351 @mkdir(AT_CONTENT_DIR . 'import/' . $this->course_id);
352 $this->import_dir = AT_CONTENT_DIR . 'import/' . $this->course_id . '/';
354 // 2. extract the backup
355 $archive = new PclZip(AT_BACKUP_DIR . $from_course_id. '/' . $my_backup['system_file_name']. '.zip');
356 if ($archive->extract( PCLZIP_OPT_PATH, $this->import_dir,
357 PCLZIP_CB_PRE_EXTRACT, 'preImportCallBack') == 0) {
358 die("Error : ".$archive->errorInfo(true));
361 // 3. get the course's max_quota. if backup is too big AND we want to import files then abort/return FALSE
362 /* get the course's max_quota */
363 // $this->getFilesSize();
365 // 4. figure out version number
366 $this->version = $this->getVersion();
367 if (!$this->version) {
368 clr_dir($this->import_dir);
370 $msg->addError('BACKUP_RESTORE');
371 header('Location: '.$_SERVER['PHP_SELF']);
373 //exit('version not found. backups < 1.3 are not supported.');
376 if (version_compare($this->version, VERSION, '>') == 1) {
377 clr_dir($this->import_dir);
380 $msg->addError('BACKUP_UNSUPPORTED_GREATER_VERSION');
381 header('Location: '.$_SERVER['PHP_SELF']);
384 if (version_compare($this_version, '1.5.3', '<')) {
385 if (file_exists($this->import_dir . 'resource_categories.csv')) {
386 @rename($this->import_dir . 'resource_categories.csv', $this->import_dir. 'links_categories.csv');
388 if (file_exists($this->import_dir . 'resource_links.csv')) {
389 @rename($this->import_dir . 'resource_links.csv', $this->import_dir. 'links.csv');
393 // 5. if override is set then delete the content
394 if ($action == 'overwrite') {
395 require_once(AT_INCLUDE_PATH.'../mods/_core/properties/lib/delete_course.inc.php');
396 delete_course($this->course_id, $material);
397 $_SESSION['s_cid'] = 0;
398 } // else: appending content
400 if ($material === TRUE) {
401 // restore the entire backup (used when creating a new course)
402 $module_list = $moduleFactory->getModules(AT_MODULE_ENABLED | AT_MODULE_CORE);
403 $_POST['material'] = $module_list;
405 foreach ($_POST['material'] as $module_name => $garbage) {
406 // restore course properties, ONLY BANNER FOR NOW.
407 if ($module_name == 'properties' && file_exists($this->import_dir . "properties.csv"))
411 $fp = @fopen($this->import_dir . "properties.csv", 'rb');
413 if (($row = @fgetcsv($fp, 70000)) !== false)
415 //hack for http://www.atutor.ca/atutor/mantis/view.php?id=3839
416 $row[0] = preg_replace('/\\\\r\\\\n/', "\r\n", $row[0]);
418 $sql = "UPDATE ".TABLE_PREFIX."courses
419 SET banner = '". mysql_real_escape_string($row[0]). "'
420 WHERE course_id = ".$this->course_id;
421 $result = mysql_query($sql,$db) or die(mysql_error());
426 $module = $moduleFactory->getModule($module_name);
427 $module->restore($this->course_id, $this->version, $this->import_dir);
429 clr_dir($this->import_dir);
434 function restore_files() {
435 $sql = "SELECT max_quota FROM ".TABLE_PREFIX."courses WHERE course_id=$this->course_id";
436 $result = mysql_query($sql, $this->db);
437 $row = mysql_fetch_assoc($result);
439 if ($row['max_quota'] != AT_COURSESIZE_UNLIMITED) {
440 global $MaxCourseSize, $MaxCourseFloat;
442 if ($row['max_quota'] == AT_COURSESIZE_DEFAULT) {
443 $row['max_quota'] = $MaxCourseSize;
446 $totalBytes = dirsize($this->import_dir . 'content/');
448 $course_total = dirsize(AT_CONTENT_DIR . $this->course_id . '/');
450 $total_after = $row['max_quota'] - $course_total - $totalBytes + $MaxCourseFloat;
452 if ($total_after < 0) {
453 //debug('not enough space. delete everything');
454 // remove the content dir, since there's no space for it
455 clr_dir($this->import_dir);
460 copys($this->import_dir.'content/', AT_CONTENT_DIR . $this->course_id);