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