remove old readme
[atutor.git] / docs / mods / _core / backups / classes / Backup.class.php
1 <?php
2 /************************************************************************/
3 /* ATutor                                                                                                                               */
4 /************************************************************************/
5 /* Copyright (c) 2002-2010                                              */
6 /* Inclusive Design Institute                                           */
7 /* http://atutor.ca                                                     */
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$
13
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
16
17 define('NUMBER',        1);
18 define('TEXT',          2);
19
20 /**
21 * Backup
22 * Class for creating and managing course backups
23 * @access       public
24 * @author       Joel Kronenberg
25 * @package      Backup
26 */
27 class Backup {
28
29         // private
30         // number of backups in the backup dir
31         var $num_backups;
32
33         // private
34         // the current course id
35         var $course_id;
36
37         // private
38         // where to store the backup
39         var $backup_dir;
40
41         // private
42         // db handler
43         var $db;
44
45         // the backup zipfile Object
46         var $zipfile;
47
48         // the timestamp for the zip files
49         var $timestamp;
50
51         // private
52         // array of installed modules that support backups
53         var $modules;
54
55         var $backup_tables;
56
57         // constructor
58         function Backup(&$db, $course_id = 0) {
59
60                 $this->db = $db;
61
62                 $this->setCourseID($course_id);
63         }
64
65         // public
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;
70         }
71
72
73         // public
74         // call staticly
75         function generateFileName( ) {
76                 global $system_courses;
77                 $title = $system_courses[$this->course_id]['title'];
78
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);
84
85                 $title .= '_' . date('d_M_y') . '.zip';
86
87                 return $title;
88         }
89
90         // public
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;
95
96                 if ($this->getNumAvailable() >= AT_COURSE_BACKUPS) {
97                         return FALSE;
98                 }
99
100                 $timestamp = time();
101
102                 $zipfile = new zipfile();
103
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);
106
107                 // backup course properties. ONLY BANNER FOR NOW.
108                 require_once(AT_INCLUDE_PATH . 'classes/CSVExport.class.php');
109                 $CSVExport = new CSVExport();
110                 $now = time();
111                 
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);
117
118                 // backup modules
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);
124                 }
125                 $zipfile->close();
126
127                 $system_file_name = md5($timestamp);
128                 
129                 if (!is_dir(AT_BACKUP_DIR)) {
130                         @mkdir(AT_BACKUP_DIR);
131                 }
132
133                 if (!is_dir(AT_BACKUP_DIR . $this->course_id)) {
134                         @mkdir(AT_BACKUP_DIR . $this->course_id);
135                 }
136
137                 $zipfile->write_file(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $system_file_name . '.zip');
138
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();
144
145                 $this->add($row);
146
147                 return TRUE;
148         }
149
150         // public
151         function upload($_FILES, $description) {
152                 global $addslashes, $msg;
153         
154                 $ext = pathinfo($_FILES['file']['name']);
155                 $ext = $ext['extension'];
156
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); 
161                         } else {
162                                 $msg->addError('FILE_NOT_SELECTED');
163                         }
164                 }
165
166                 if ($_FILES['file']['size'] == 0) {
167                         $msg->addError('IMPORTFILE_EMPTY');
168                 }
169
170                 if($msg->containsErrors()) {
171                         return;
172                 }
173
174                 $row = array();
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']);
180
181                 if (!is_dir(AT_BACKUP_DIR)) {
182                         @mkdir(AT_BACKUP_DIR);
183                 }
184
185                 if (!is_dir(AT_BACKUP_DIR . $this->course_id)) {
186                         @mkdir(AT_BACKUP_DIR . $this->course_id);
187                 }
188
189                 $backup_path = AT_BACKUP_DIR . DIRECTORY_SEPARATOR . $this->course_id . DIRECTORY_SEPARATOR;
190
191                 move_uploaded_file($_FILES['file']['tmp_name'], $backup_path . $row['system_file_name'].'.zip');
192
193                 $this->add($row);
194
195                 return;
196         }
197
198         // private
199         // adds a backup to the database
200         function add($row) {
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);
203         }
204
205         // public
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;
211                 }
212
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);
216
217                 $this->num_backups = $row['cnt'];
218                 return $row['cnt'];
219         }
220
221         // public
222         // get list of backups
223         function getAvailableList() {
224                 $backup_list = array();
225
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']);
231                 }
232
233                 $this->num_backups = count($backup_list);
234
235                 return $backup_list;
236         }
237
238         // public
239         function download($backup_id) { // or fetch()
240                 $list = $this->getAvailableList($this->course_id);
241                 if (!isset($list[$backup_id])) {
242                         // catch the error
243                         //debug('does not belong to us');
244                         exit;
245                 }
246
247                 $my_backup = $list[$backup_id];
248                 $file_name = $my_backup['file_name'];
249
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']);
257
258                 // see the note in get.php about the use of x-Sendfile
259                 ob_end_clean();
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
263
264                 readfile_in_chunks(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
265                 exit;
266         }
267
268         // public
269         function delete($backup_id) {
270                 $list = $this->getAvailableList($this->course_id);
271                 if (!isset($list[$backup_id])) {
272                         // catch the error
273                         //debug('does not belong to us');
274                         exit;
275                 }
276                 $my_backup = $list[$backup_id];
277
278                 // delete the backup file:
279                 @unlink(AT_BACKUP_DIR . $this->course_id . DIRECTORY_SEPARATOR . $my_backup['system_file_name']. '.zip');
280
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);
284         }
285
286         // public
287         function edit($backup_id, $description) {
288                 global $addslashes;
289
290                 // sql safe input
291                 $backup_id              = abs($backup_id);
292                 $description    = $addslashes($description);
293
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);
297
298         }
299
300         // public
301         function getRow($backup_id, $course_id = 0) {
302                 // sql safe input
303                 $backup_id      = abs($backup_id);
304                 $course_id      = abs($course_id);
305
306                 if ($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";
308                 } else {
309                         $sql    = "SELECT *, UNIX_TIMESTAMP(date) AS date_timestamp FROM ".TABLE_PREFIX."backups WHERE backup_id=$backup_id AND course_id=$this->course_id";
310                 }
311
312                 $result = mysql_query($sql, $this->db);
313                 $row = mysql_fetch_assoc($result);
314
315                 if ($row) {
316                         $row['contents'] = unserialize($row['contents']);
317                 }
318                 return $row;
319         }
320
321         // public
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);
326
327                 return $input;
328         }
329
330         // public
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]);
334                 } else {
335                         return false;
336                 }
337         }
338
339         // public
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');
344
345                 if (!$from_course_id) {
346                         $from_course_id = $this->course_id;
347                 }
348
349                 // 1. get backup row/information
350                 $my_backup = $this->getRow($backup_id, $from_course_id);
351
352                 @mkdir(AT_CONTENT_DIR . 'import/' . $this->course_id);
353                 $this->import_dir = AT_CONTENT_DIR . 'import/' . $this->course_id . '/';
354
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));
360                 }
361
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();
365
366                 // 4. figure out version number
367                 $this->version = $this->getVersion();
368                 if (!$this->version) {
369                         clr_dir($this->import_dir);
370                         global $msg;
371                         $msg->addError('BACKUP_RESTORE');
372                         header('Location: '.$_SERVER['PHP_SELF']);
373                         exit;
374                         //exit('version not found. backups < 1.3 are not supported.');
375                 }
376
377                 if (version_compare($this->version, VERSION, '>') == 1) {
378                         clr_dir($this->import_dir);
379                         global $msg;
380
381                         $msg->addError('BACKUP_UNSUPPORTED_GREATER_VERSION');
382                         header('Location: '.$_SERVER['PHP_SELF']);
383                         exit;
384                 }
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');
388                         }
389                         if (file_exists($this->import_dir . 'resource_links.csv')) {
390                                 @rename($this->import_dir . 'resource_links.csv', $this->import_dir. 'links.csv');
391                         }
392                 }
393
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
400
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;
405                 }
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"))
409                         {
410                                 global $db;
411                                 
412                                 $fp = @fopen($this->import_dir . "properties.csv", 'rb');
413
414                                 if (($row = @fgetcsv($fp, 70000)) !== false)
415                                 {
416                                         //hack for http://www.atutor.ca/atutor/mantis/view.php?id=3839
417                                         $row[0] = preg_replace('/\\\\r\\\\n/', "\r\n", $row[0]);
418
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());
423                                 }
424                         }
425                         
426                         // restore modules
427                         $module = $moduleFactory->getModule($module_name);
428                         $module->restore($this->course_id, $this->version, $this->import_dir);
429                 }
430                 clr_dir($this->import_dir);
431         }
432
433         // private
434         // no longer used
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);
439
440                 if ($row['max_quota'] != AT_COURSESIZE_UNLIMITED) {
441                         global $MaxCourseSize, $MaxCourseFloat;
442
443                         if ($row['max_quota'] == AT_COURSESIZE_DEFAULT) {
444                                 $row['max_quota'] = $MaxCourseSize;
445                         }
446                         
447                         $totalBytes   = dirsize($this->import_dir . 'content/');
448                         
449                         $course_total = dirsize(AT_CONTENT_DIR . $this->course_id . '/');
450                 
451                         $total_after  = $row['max_quota'] - $course_total - $totalBytes + $MaxCourseFloat;
452
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);
457                                 return FALSE;
458                         }
459                 }
460
461                 copys($this->import_dir.'content/', AT_CONTENT_DIR . $this->course_id);
462         }
463 }
464
465 ?>