made a copy
[atutor.git] / include / classes / Module / Module.class.php
1 <?php
2 /************************************************************************/
3 /* ATutor                                                                                                                               */
4 /************************************************************************/
5 /* Copyright (c) 2002-2008 by Greg Gay, Joel Kronenberg & Heidi Hazelton*/
6 /* Adaptive Technology Resource Centre / University of Toronto                  */
7 /* http://atutor.ca                                                                                                             */
8 /*                                                                                                                                              */
9 /* This program is free software. You can redistribute it and/or                */
10 /* modify it under the terms of the GNU General Public License                  */
11 /* as published by the Free Software Foundation.                                                */
12 /************************************************************************/
13 // $Id$
14
15 define('AT_MODULE_STATUS_DISABLED',    1);
16 define('AT_MODULE_STATUS_ENABLED',     2);
17 define('AT_MODULE_STATUS_MISSING',     4);
18 define('AT_MODULE_STATUS_UNINSTALLED', 8); // not in the db
19 define('AT_MODULE_STATUS_PARTIALLY_UNINSTALLED', 16); // not in the db
20
21 define('AT_MODULE_TYPE_CORE',     1);
22 define('AT_MODULE_TYPE_STANDARD', 2);
23 define('AT_MODULE_TYPE_EXTRA',    4);
24
25 define('AT_MODULE_DIR_CORE',     '_core');
26 define('AT_MODULE_DIR_STANDARD', '_standard');
27
28 define('AT_MODULE_PATH', realpath(AT_INCLUDE_PATH.'../mods') . DIRECTORY_SEPARATOR);
29
30 /**
31 * ModuleFactory //
32
33 * @access       public
34 * @author       Joel Kronenberg
35 * @package      Module
36 */
37 class ModuleFactory {
38         // private
39         var $_modules = NULL; // array of module refs
40
41         function ModuleFactory($auto_load = FALSE) {
42                 global $db;
43
44                 /* snippit to use when extending Module classes:
45                 $sql    = "SELECT dir_name, privilege, admin_privilege, status FROM ". TABLE_PREFIX . "modules WHERE status=".AT_MODULE_STATUS_ENABLED;
46                 $result = mysql_query($sql, $db);
47                 $row = mysql_fetch_assoc($result);
48                 require(AT_MODULE_PATH . $row['dir_name'].'/module.php');
49                 $module =& new PropertiesModule($row);
50                 ***/
51
52                 $this->_modules = array();
53
54                 if ($auto_load == TRUE) {
55                         // initialise enabled modules
56                         $sql    = "SELECT dir_name, privilege, admin_privilege, status, cron_interval, cron_last_run FROM ". TABLE_PREFIX . "modules WHERE status=".AT_MODULE_STATUS_ENABLED;
57                         $result = mysql_query($sql, $db);
58                         while($row = mysql_fetch_assoc($result)) {
59                                 $module =& new Module($row);
60                                 $this->_modules[$row['dir_name']] =& $module;
61                                 $module->load();
62                         }
63                 }
64         }
65
66         // public
67         // status := enabled | disabled | uninstalled | missing
68         // type  := core | standard | extra
69         // sort  := true | false (by name only)
70         // the results of this method are not cached. call sparingly.
71         function & getModules($status, $type = 0, $sort = FALSE) {
72                 global $db;
73
74                 $modules     = array();
75                 $all_modules = array();
76
77                 if ($type == 0) {
78                         $type = AT_MODULE_TYPE_CORE | AT_MODULE_TYPE_STANDARD | AT_MODULE_TYPE_EXTRA;
79                 }
80
81                 $sql    = "SELECT dir_name, privilege, admin_privilege, status, cron_interval, cron_last_run FROM ". TABLE_PREFIX . "modules";
82                 $result = mysql_query($sql, $db);
83                 while($row = mysql_fetch_assoc($result)) {
84                         if (!isset($this->_modules[$row['dir_name']])) {
85                                 $module =& new Module($row);
86                         } else {
87                                 $module =& $this->_modules[$row['dir_name']];
88                         }
89                         $all_modules[$row['dir_name']] =& $module;
90                 }
91
92                 // small performance addition:
93                 if ($status & AT_MODULE_STATUS_UNINSTALLED) {
94                         $dir = opendir(AT_MODULE_PATH);
95                         while (false !== ($dir_name = readdir($dir))) {
96                                 if (($dir_name == '.') 
97                                         || ($dir_name == '..') 
98                                         || ($dir_name == '.svn') 
99                                         || ($dir_name == AT_MODULE_DIR_CORE) 
100                                         || ($dir_name == AT_MODULE_DIR_STANDARD)) {
101                                         continue;
102                                 }
103
104                                 if (is_dir(AT_MODULE_PATH . $dir_name) && !isset($all_modules[$dir_name])) {
105                                         $module =& new Module($dir_name);
106                                         $all_modules[$dir_name] =& $module;
107                                 }
108                         }
109                         closedir($dir);
110                 }
111
112                 $keys = array_keys($all_modules);
113                 foreach ($keys as $dir_name) {
114                         $module =& $all_modules[$dir_name];
115                         if ($module->checkStatus($status) && $module->checkType($type)) {
116                                 $modules[$dir_name] =& $module;
117                         }
118                 }
119
120                 if ($sort) {
121                         uasort($modules, array($this, 'compare'));
122                 }
123                 return $modules;
124         }
125
126         // public.
127         function & getModule($module_dir) {
128                 if (!isset($this->_modules[$module_dir])) {
129                         global $db;
130                         $sql    = "SELECT dir_name, privilege, admin_privilege, status FROM ". TABLE_PREFIX . "modules WHERE dir_name='$module_dir'";
131                         $result = mysql_query($sql, $db);
132                         if ($row = mysql_fetch_assoc($result)) {
133                                 $module =& new Module($row);
134                         } else {
135                                 $module =& new Module($module_dir);
136                         }
137                         $this->_modules[$module_dir] =& $module;
138                 }
139                 return $this->_modules[$module_dir];
140         }
141
142         // private
143         // used for sorting modules
144         function compare($a, $b) {
145                 return strnatcasecmp($a->getName(), $b->getName());
146         }
147 }
148
149 /**
150 * Module
151
152 * @access       public
153 * @author       Joel Kronenberg
154 * @package      Module
155 */
156 class Module {
157         // private
158         var $_moduleObj;
159         var $_directoryName;
160         var $_status; // core|enabled|disabled
161         var $_privilege; // priv bit(s) | 0 (in dec form)
162         var $_admin_privilege; // priv bit(s) | 0 (in dec form)
163         var $_display_defaults; // bit(s)
164         var $_pages;
165         var $_type; // core, standard, extra
166         var $_properties; // array from xml
167         var $_cron_interval; // cron interval
168         var $_cron_last_run; // cron last run date stamp
169
170         // constructor
171         function Module($row) {
172                 if (is_array($row)) {
173                         $this->_directoryName   = $row['dir_name'];
174                         $this->_status          = $row['status'];
175                         $this->_privilege       = $row['privilege'];
176                         $this->_admin_privilege = $row['admin_privilege'];
177                         $this->_display_defaults= isset($row['display_defaults']) ? $row['display_defaults'] : 0;
178                         $this->_cron_interval   = $row['cron_interval'];
179                         $this->_cron_last_run   = $row['cron_last_run'];
180
181                         if (strpos($row['dir_name'], AT_MODULE_DIR_CORE) === 0) {
182                                 $this->_type = AT_MODULE_TYPE_CORE;
183                         } else if (strpos($row['dir_name'], AT_MODULE_DIR_STANDARD) === 0) {
184                                 $this->_type = AT_MODULE_TYPE_STANDARD;
185                         } else {
186                                 $this->_type = AT_MODULE_TYPE_EXTRA;
187                         }
188                 } else {
189                         $this->_directoryName   = $row;
190                         $this->_status          = AT_MODULE_STATUS_UNINSTALLED;
191                         $this->_privilege       = 0;
192                         $this->_admin_privilege = 0;
193                         $this->_display_defaults= 0;
194                         $this->_type            = AT_MODULE_TYPE_EXTRA; // standard/core are installed by default
195                 }
196         }
197
198         // statuses
199         function checkStatus($status) { return (bool) ($status & $this->_status); }
200         function isPartiallyUninstalled()  { return ($this->_status == AT_MODULE_STATUS_PARTIALLY_UNINSTALLED) ? true : false; }
201         function isUninstalled()  { return ($this->_status == AT_MODULE_STATUS_UNINSTALLED) ? true : false; }
202         function isEnabled()      { return ($this->_status == AT_MODULE_STATUS_ENABLED)     ? true : false; }
203         function isDisabled()     { return ($this->_status == AT_MODULE_STATUS_DISABLED)    ? true : false; }
204         function isMissing()      { return ($this->_status == AT_MODULE_STATUS_MISSING)     ? true : false; }
205
206         // types
207         function checkType($type) { return (bool) ($type & $this->_type); }
208         function isCore()     { return ($this->_type == AT_MODULE_TYPE_CORE)     ? true : false; }
209         function isStandard() { return ($this->_type == AT_MODULE_TYPE_STANDARD) ? true : false; }
210         function isExtra()    { return ($this->_type == AT_MODULE_TYPE_EXTRA)    ? true : false; }
211
212         // privileges
213         function getPrivilege()      { return $this->_privilege;       }
214         function getAdminPrivilege() { return $this->_admin_privilege; }
215
216         function load() {
217                 if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module.php')) {
218                         global $_modules, $_pages, $_stacks, $_list;  // $_list is for sublinks on "detail view"
219
220                         require(AT_MODULE_PATH . $this->_directoryName.'/module.php');
221
222                         if (isset($this->_pages)) {
223                                 $_pages = array_merge_recursive((array) $_pages, $this->_pages);
224                         }
225
226                         //side menu items
227                         if (isset($this->_stacks)) {
228                                 $count = 0;
229                                 $_stacks = array_merge((array)$_stacks, $this->_stacks);
230                         }
231
232                         // sublinks on "detail view"
233                         if(isset($this->_list)) {
234                                 $_list = array_merge((array)$_list, $this->_list);                      
235                         }
236                         
237                         //student tools
238                         if (isset($_student_tool)) {
239                                 $this->_student_tool =& $_student_tool;
240                                 $_modules[] = $this->_student_tool;
241                         }
242
243                         //group tools
244                         if (isset($_group_tool)) {
245                                 $this->_group_tool =& $_group_tool;
246                         }
247                 }                                       
248         }
249
250         // private
251         function _initModuleProperties() {
252                 if (!isset($this->_properties)) {
253                         require_once(dirname(__FILE__) . '/ModuleParser.class.php');
254                         $moduleParser   =& new ModuleParser();
255                         $moduleParser->parse(@file_get_contents(AT_MODULE_PATH . $this->_directoryName.'/module.xml'));
256                         if ($moduleParser->rows[0]) {
257                                 $this->_properties = $moduleParser->rows[0];
258                         } else {
259                                 $this->_properties = array();
260                                 $this->setIsMissing(); // the xml file may not be found -> the dir may be missing.
261                         }
262                 }
263         }
264
265         /**
266         * Get the properties of this module as found in the module.xml file
267         * @access  public
268         * @param   array $properties_list       list of property names
269         * @return  array associative array of property/value pairs
270         * @author  Joel Kronenberg
271         */
272         function getProperties($properties_list) {
273                 $this->_initModuleProperties();
274
275                 if (!$this->_properties) {
276                         return;
277                 }
278                 $properties_list = array_flip($properties_list);
279                 foreach ($properties_list as $property => $garbage) {
280                         $properties_list[$property] = $this->_properties[$property];
281                 }
282                 return $properties_list;
283         }
284         /**
285         * Get a single property as found in the module.xml file
286         * @access  public
287         * @param   string $property     name of the property to return
288         * @return  string the value of the property 
289         * @author  Joel Kronenberg
290         */
291         function getProperty($property) {
292                 $this->_initModuleProperties();
293
294                 if (!$this->_properties) {
295                         return;
296                 }
297
298                 return $this->_properties[$property];
299         }
300
301         function getCronInterval() {
302                 return $this->_cron_interval;
303
304         }
305
306         function getName() {
307                 if ($this->isUninstalled()) {
308                         $name = $this->getProperty('name');
309                         return current($name);
310                 }
311                 return _AT(basename($this->_directoryName));
312         }
313
314         function getDescription($lang = 'en') {
315                 $this->_initModuleProperties();
316
317                 if (!$this->_properties) {
318                         return;
319                 }
320
321                 if (isset($this->_properties['description'][$lang])) {
322                         return $this->_properties['description'][$lang];
323                 }
324                 $description = current($this->_properties['description']);
325                 return $description;
326         }
327
328         function getChildPage($page) {
329                 if (!is_array($this->_pages)) {
330                         return;
331                 }
332                 foreach ($this->_pages as $tmp_page => $item) {
333                         if (!empty($item['parent']) && $item['parent'] == $page) {
334                                 return $tmp_page;
335                         }
336                 }
337         }
338
339         /**
340         * Checks whether or not this module can be backed-up
341         * @access  public
342         * @return  boolean true if this module can be backed-up, false otherwise
343         * @author  Joel Kronenberg
344         */
345         function isBackupable() {
346                 return is_file(AT_MODULE_PATH . $this->_directoryName.'/module_backup.php');
347         }
348
349         function createGroup($group_id) {
350                 if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php')) {
351                         require_once(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php');
352                         $fn_name = basename($this->_directoryName) .'_create_group';
353                         $fn_name($group_id);
354                 }
355         }
356
357         function deleteGroup($group_id) {
358                 $fn_name = basename($this->_directoryName) .'_delete_group';
359
360                 if (!function_exists($fn_name) && is_file(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php')) {
361                         require_once(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php');
362                 } 
363                 if (function_exists($fn_name)) {
364                         $fn_name($group_id);
365                 }
366         }
367
368         function getGroupTool() {
369                 if (!isset($this->_group_tool)) {
370                         return;
371                 } 
372
373                 return $this->_group_tool;
374         }
375
376         function isGroupable() {
377                 return is_file(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php');
378         }
379
380         /**
381         * Backup this module for a given course
382         * @access  public
383         * @param   int          $course_id      ID of the course to backup
384         * @param   object       $zipfile        a reference to a zipfile object
385         * @author  Joel Kronenberg
386         */
387         function backup($course_id, &$zipfile) {
388                 static $CSVExport;
389
390                 if (!isset($CSVExport)) {
391                         require_once(AT_INCLUDE_PATH . 'classes/CSVExport.class.php');
392                         $CSVExport = new CSVExport();
393                 }
394                 $now = time();
395
396                 if ($this->isBackupable()) {
397                         require(AT_MODULE_PATH . $this->_directoryName . '/module_backup.php');
398                         if (isset($sql)) {
399                                 foreach ($sql as $file_name => $table_sql) {
400                                         $content = $CSVExport->export($table_sql, $course_id);
401                                         if ($content) {
402                                                 $zipfile->add_file($content, $file_name . '.csv', $now);
403                                         }
404                                 }
405                         }
406
407                         if (isset($dirs)) {
408                                 foreach ($dirs as $dir => $path) {
409                                         $path = str_replace('?', $course_id, $path);
410
411                                         $zipfile->add_dir($path , $dir);
412                                 }
413                         }
414                 }
415         }
416
417         /**
418         * Restores this module into the given course
419         * @access  public
420         * @param   int          $course_id      ID of the course to restore into
421         * @param   string       $version        version number of the ATutor installation used to make this backup
422         * @param   string       $import_dir     the path to the import directory
423         * @author  Joel Kronenberg
424         */
425         function restore($course_id, $version, $import_dir) {
426                 static $CSVImport;
427                 if (!file_exists(AT_MODULE_PATH . $this->_directoryName.'/module_backup.php')) {
428                         return;
429                 }
430
431                 if (!isset($CSVImport)) {
432                         require_once(AT_INCLUDE_PATH . 'classes/CSVImport.class.php');
433                         $CSVImport = new CSVImport();
434                 }
435
436                 require(AT_MODULE_PATH . $this->_directoryName.'/module_backup.php');
437
438                 if (isset($sql)) {
439                         foreach ($sql as $table_name => $table_sql) {
440                                 $CSVImport->import($table_name, $import_dir, $course_id, $version);
441                         }
442                 }
443                 if (isset($dirs)) {
444                         foreach ($dirs as $src => $dest) {
445                                 $dest = str_replace('?', $course_id, $dest);
446                                 copys($import_dir.$src, $dest);
447                         }
448                 }
449         }
450
451         /**
452         * Delete this module's course content. If $groups is specified then it will
453         * delete all content for the groups specified.
454         * @access  public
455         * @param   int   $course_id     ID of the course to delete
456         * @param   array $groups    Array of groups to delete
457         * @author  Joel Kronenberg
458         */
459         function delete($course_id, $groups) {
460                 if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module_delete.php')) {
461                         require(AT_MODULE_PATH . $this->_directoryName.'/module_delete.php');
462                         if (function_exists(basename($this->_directoryName).'_delete')) {
463                                 $fnctn = basename($this->_directoryName).'_delete';
464                                 $fnctn($course_id);
465                         }
466                 }
467                 if ($groups) {
468                         foreach ($groups as $group_id) {
469                                 $this->deleteGroup($group_id);
470                         }
471                 }
472         }
473
474         /**
475         * Enables the installed module
476         * @access  public
477         * @author  Joel Kronenberg
478         */
479         function enable() {
480                 global $db;
481
482                 $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_ENABLED.' WHERE dir_name="'.$this->_directoryName.'"';
483                 $result = mysql_query($sql, $db);
484         }
485
486         /**
487         * Sets the status to missing if the module dir doesn't exist.
488         * @access  public
489         * @param   boolean $force whether or not to force the module to be missing (used for bundled extra modules upon upgrade)
490         * @author  Joel Kronenberg
491         */
492         function setIsMissing($force = false) {
493                 global $db;
494                 // if the directory doesn't exist then set the status to MISSING
495                 if ($force || !is_dir(AT_MODULE_PATH . $this->_directoryName)) {
496                         $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_MISSING.' WHERE dir_name="'.$this->_directoryName.'"';
497                         $result = mysql_query($sql, $db);
498                 }
499         }
500
501         /**
502         * Disables the installed module
503         * @access  public
504         * @author  Joel Kronenberg
505         */
506         function disable() {
507                 global $db;
508
509                 // remove any privileges admins, students
510                 if ($this->_privilege > 1) {
511                         $sql = 'UPDATE '. TABLE_PREFIX . 'course_enrollment SET `privileges`=`privileges`-'.$this->_privilege.' WHERE `privileges` > 1 AND (`privileges` & '.$this->_privilege.')<>0';
512                         $result = mysql_query($sql, $db);
513                 }
514
515                 if ($this->_admin_privilege > 1) {
516                         $sql = 'UPDATE '. TABLE_PREFIX . 'admins SET `privileges`=`privileges`-'.$this->_admin_privilege.' WHERE `privileges` > 1 AND (`privileges` & '.$this->_admin_privilege.')<>0';
517                         $result = mysql_query($sql, $db);
518                 }
519
520                 $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_DISABLED.' WHERE dir_name="'.$this->_directoryName.'"';
521                 $result = mysql_query($sql, $db);
522
523                 if (function_exists(basename($this->_directoryName).'_disable')) {
524                         $fn_name = basename($this->_directoryName).'_disable';
525                         $fn_name();
526                 }
527         }
528
529         /**
530         * Installs the module
531         * @access  public
532         * @author  Joel Kronenberg
533         */
534         function install() {
535                 global $msg;
536
537                 // should check if this module is already installed...
538
539                 if (file_exists(AT_MODULE_PATH . $this->_directoryName . '/module_install.php')) {
540                         require(AT_MODULE_PATH . $this->_directoryName . '/module_install.php');
541                 }
542
543                 if (!$msg->containsErrors()) {
544                         global $db;
545
546                         $sql = "SELECT MAX(`privilege`) AS `privilege`, MAX(admin_privilege) AS admin_privilege FROM ".TABLE_PREFIX."modules";
547                         $result = mysql_query($sql, $db);
548                         $row = mysql_fetch_assoc($result);
549
550                         if (($_course_privilege === TRUE) || ((string) $_course_privilege == 'new')) {
551                                 $priv = $row['privilege'] * 2;
552                         } else if ($_course_privilege == AT_PRIV_ADMIN) {
553                                 $priv = AT_PRIV_ADMIN;
554                         } else {
555                                 $priv = 0;
556                         }
557
558                         if (($_admin_privilege === TRUE) || ((string) $_admin_privilege == 'new')) {
559                                 $admin_priv = $row['admin_privilege'] * 2;
560                         } else {
561                                 $admin_priv = AT_ADMIN_PRIV_ADMIN;
562                         }
563
564                         if (isset($_cron_interval)) {
565                                 $_cron_interval = abs($_cron_interval);
566                         } else {
567                                 $_cron_interval = 0;
568                         }
569
570                         $sql = 'INSERT INTO '. TABLE_PREFIX . 'modules VALUES ("'.$this->_directoryName.'", '.AT_MODULE_STATUS_DISABLED.', '.$priv.', '.$admin_priv.', '.$_cron_interval.', 0)';
571                         mysql_query($sql, $db);
572                         if (mysql_affected_rows($db) != 1) {
573                                 // in case this module has to be re-installed (because it was Missing)
574                                 $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_DISABLED.' WHERE dir_name="'.$this->_directoryName.'"';
575                                 mysql_query($sql, $db);
576                         }
577                 }
578         }
579
580         /**
581         * Uninstalls the module
582         * @access  public
583         * @author  Cindy Qi Li
584         */
585         function uninstall($del_data='') {
586                 global $msg;
587
588                 if (file_exists(AT_MODULE_PATH . $this->_directoryName . '/module_uninstall.php') && $del_data == 1) 
589                 {
590                         require(AT_MODULE_PATH . $this->_directoryName . '/module_uninstall.php');
591                 }
592
593                 if (!$msg->containsErrors()) 
594                 {
595                         require_once(AT_INCLUDE_PATH.'lib/filemanager.inc.php');
596                         
597                         if (!clr_dir(AT_MODULE_PATH . $this->_directoryName))
598                                 $msg->addError(array('MODULE_UNINSTALL', '<li>'.AT_MODULE_PATH . $this->_directoryName.' can not be removed. Please manually remove it.</li>'));
599                 }
600                 
601                 if (!$msg->containsErrors()) 
602                 {
603                         global $db;
604
605                         $sql = "DELETE FROM ". TABLE_PREFIX . "modules WHERE dir_name = '".$this->_directoryName."'";
606                         mysql_query($sql, $db);
607                 }
608
609                 if ($msg->containsErrors()) 
610                 {
611                         global $db;
612
613                         $sql = "UPDATE ". TABLE_PREFIX . "modules SET status=".AT_MODULE_STATUS_PARTIALLY_UNINSTALLED." WHERE dir_name='".$this->_directoryName."'";
614                         mysql_query($sql, $db);
615                 }
616         }
617
618         function getStudentTools() {
619                 if (!isset($this->_student_tool)) {
620                         return FALSE;
621                 } 
622
623                 return $this->_student_tool;
624         }
625
626
627         function runCron() {
628                 if ( ($this->_cron_last_run + ($this->_cron_interval * 60)) < time()) {
629                         if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module_cron.php')) {
630                                 require(AT_MODULE_PATH . $this->_directoryName.'/module_cron.php');
631                                 if (function_exists(basename($this->_directoryName).'_cron')) {
632                                         $fnctn = basename($this->_directoryName).'_cron';
633                                         $fnctn();
634                                 }
635                         }
636                         $this->updateCronLastRun();
637                 }
638         }
639
640         // i'm private! update the last time the cron was run
641         function updateCronLastRun() {
642                 global $db;
643
644                 $sql = "UPDATE ".TABLE_PREFIX."modules SET cron_last_run=".time()." WHERE dir_name='$this->_directoryName'";
645                 mysql_query($sql, $db);
646
647         }
648 }
649
650 ?>