2 /************************************************************************/
4 /************************************************************************/
5 /* Copyright (c) 2002-2008 by Greg Gay, Joel Kronenberg & Heidi Hazelton*/
6 /* Adaptive Technology Resource Centre / University of Toronto */
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 /************************************************************************/
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
21 define('AT_MODULE_TYPE_CORE', 1);
22 define('AT_MODULE_TYPE_STANDARD', 2);
23 define('AT_MODULE_TYPE_EXTRA', 4);
25 define('AT_MODULE_DIR_CORE', '_core');
26 define('AT_MODULE_DIR_STANDARD', '_standard');
28 define('AT_MODULE_PATH', realpath(AT_INCLUDE_PATH.'../mods') . DIRECTORY_SEPARATOR);
34 * @author Joel Kronenberg
39 var $_modules = NULL; // array of module refs
41 function ModuleFactory($auto_load = FALSE) {
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);
52 $this->_modules = array();
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;
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) {
75 $all_modules = array();
78 $type = AT_MODULE_TYPE_CORE | AT_MODULE_TYPE_STANDARD | AT_MODULE_TYPE_EXTRA;
81 $sql = "SELECT dir_name, privilege, admin_privilege, status, cron_interval, cron_last_run FROM ". TABLE_PREFIX . "modules";
82 $result = mysql_query($sql, $db);
84 while($row = mysql_fetch_assoc($result)) {
85 if (!isset($this->_modules[$row['dir_name']])) {
86 $module = new Module($row);
88 $module = $this->_modules[$row['dir_name']];
90 $all_modules[$row['dir_name']] = $module;
93 // small performance addition:
94 if ($status & AT_MODULE_STATUS_UNINSTALLED) {
95 $dir = opendir(AT_MODULE_PATH);
96 while (false !== ($dir_name = readdir($dir))) {
97 if (($dir_name == '.')
98 || ($dir_name == '..')
99 || ($dir_name == '.svn')
100 || ($dir_name == AT_MODULE_DIR_CORE)
101 || ($dir_name == AT_MODULE_DIR_STANDARD)) {
105 if (is_dir(AT_MODULE_PATH . $dir_name) && !isset($all_modules[$dir_name])) {
106 $module = new Module($dir_name);
107 $all_modules[$dir_name] = $module;
113 $keys = array_keys($all_modules);
114 foreach ($keys as $dir_name) {
115 $module =$all_modules[$dir_name];
116 if ($module->checkStatus($status) && $module->checkType($type)) {
117 $modules[$dir_name] = $module;
122 uasort($modules, array($this, 'compare'));
129 function & getModule($module_dir) {
130 if (!isset($this->_modules[$module_dir])) {
132 $sql = "SELECT dir_name, privilege, admin_privilege, status FROM ". TABLE_PREFIX . "modules WHERE dir_name='$module_dir'";
133 $result = mysql_query($sql, $db);
134 if ($row = mysql_fetch_assoc($result)) {
135 $module = new Module($row);
137 $module = new Module($module_dir);
139 $this->_modules[$module_dir] =& $module;
141 return $this->_modules[$module_dir];
145 // used for sorting modules
146 function compare($a, $b) {
147 return strnatcasecmp($a->getName(), $b->getName());
155 * @author Joel Kronenberg
162 var $_status; // core|enabled|disabled
163 var $_privilege; // priv bit(s) | 0 (in dec form)
164 var $_admin_privilege; // priv bit(s) | 0 (in dec form)
165 var $_display_defaults; // bit(s)
167 var $_type; // core, standard, extra
168 var $_properties; // array from xml
169 var $_cron_interval; // cron interval
170 var $_cron_last_run; // cron last run date stamp
173 function Module($row) {
174 if (is_array($row)) {
175 $this->_directoryName = $row['dir_name'];
176 $this->_status = $row['status'];
177 $this->_privilege = $row['privilege'];
178 $this->_admin_privilege = $row['admin_privilege'];
179 $this->_display_defaults= isset($row['display_defaults']) ? $row['display_defaults'] : 0;
180 $this->_cron_interval = $row['cron_interval'];
181 $this->_cron_last_run = $row['cron_last_run'];
183 if (strpos($row['dir_name'], AT_MODULE_DIR_CORE) === 0) {
184 $this->_type = AT_MODULE_TYPE_CORE;
185 } else if (strpos($row['dir_name'], AT_MODULE_DIR_STANDARD) === 0) {
186 $this->_type = AT_MODULE_TYPE_STANDARD;
188 $this->_type = AT_MODULE_TYPE_EXTRA;
191 $this->_directoryName = $row;
192 $this->_status = AT_MODULE_STATUS_UNINSTALLED;
193 $this->_privilege = 0;
194 $this->_admin_privilege = 0;
195 $this->_display_defaults= 0;
196 $this->_type = AT_MODULE_TYPE_EXTRA; // standard/core are installed by default
201 function checkStatus($status) { return (bool) ($status & $this->_status); }
202 function isPartiallyUninstalled() { return ($this->_status == AT_MODULE_STATUS_PARTIALLY_UNINSTALLED) ? true : false; }
203 function isUninstalled() { return ($this->_status == AT_MODULE_STATUS_UNINSTALLED) ? true : false; }
204 function isEnabled() { return ($this->_status == AT_MODULE_STATUS_ENABLED) ? true : false; }
205 function isDisabled() { return ($this->_status == AT_MODULE_STATUS_DISABLED) ? true : false; }
206 function isMissing() { return ($this->_status == AT_MODULE_STATUS_MISSING) ? true : false; }
209 function checkType($type) { return (bool) ($type & $this->_type); }
210 function isCore() { return ($this->_type == AT_MODULE_TYPE_CORE) ? true : false; }
211 function isStandard() { return ($this->_type == AT_MODULE_TYPE_STANDARD) ? true : false; }
212 function isExtra() { return ($this->_type == AT_MODULE_TYPE_EXTRA) ? true : false; }
215 function getPrivilege() { return $this->_privilege; }
216 function getAdminPrivilege() { return $this->_admin_privilege; }
219 if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module.php')) {
220 global $_modules, $_pages, $_stacks, $_list, $_tool; // $_list is for sublinks on "detail view"
222 require(AT_MODULE_PATH . $this->_directoryName.'/module.php');
224 if (isset($this->_pages)) {
225 $_pages = array_merge_recursive((array) $_pages, $this->_pages);
229 if (isset($this->_stacks)) {
231 $_stacks = array_merge((array)$_stacks, $this->_stacks);
234 // sublinks on "detail view"
235 if(isset($this->_list)) {
236 $_list = array_merge((array)$_list, $this->_list);
239 //TODO***********BOLOGNA***********REMOVE ME***********/
240 //tool manager (content editing)
241 if(isset($this->_tool)) {
242 $_tool = array_merge((array)$_tool, $this->_tool);
246 if (isset($_student_tool)) {
247 $this->_student_tool =& $_student_tool;
248 $_modules[] = $this->_student_tool;
252 if (isset($_group_tool)) {
253 $this->_group_tool =& $_group_tool;
259 function _initModuleProperties() {
260 if (!isset($this->_properties)) {
261 require_once(dirname(__FILE__) . '/ModuleParser.class.php');
262 $moduleParser = new ModuleParser();
263 $moduleParser->parse(@file_get_contents(AT_MODULE_PATH . $this->_directoryName.'/module.xml'));
264 if ($moduleParser->rows[0]) {
265 $this->_properties = $moduleParser->rows[0];
267 $this->_properties = array();
268 $this->setIsMissing(); // the xml file may not be found -> the dir may be missing.
274 * Get the properties of this module as found in the module.xml file
276 * @param array $properties_list list of property names
277 * @return array associative array of property/value pairs
278 * @author Joel Kronenberg
280 function getProperties($properties_list) {
281 $this->_initModuleProperties();
283 if (!$this->_properties) {
286 $properties_list = array_flip($properties_list);
287 foreach ($properties_list as $property => $garbage) {
288 $properties_list[$property] = $this->_properties[$property];
290 return $properties_list;
293 * Get a single property as found in the module.xml file
295 * @param string $property name of the property to return
296 * @return string the value of the property
297 * @author Joel Kronenberg
299 function getProperty($property) {
300 $this->_initModuleProperties();
302 if (!$this->_properties) {
306 return $this->_properties[$property];
309 function getCronInterval() {
310 return $this->_cron_interval;
315 if ($this->isUninstalled()) {
316 $name = $this->getProperty('name');
317 return current($name);
319 return _AT(basename($this->_directoryName));
322 function getDescription($lang = 'en') {
323 $this->_initModuleProperties();
325 if (!$this->_properties) {
329 if (isset($this->_properties['description'][$lang])) {
330 return $this->_properties['description'][$lang];
332 $description = current($this->_properties['description']);
336 function getChildPage($page) {
337 if (!is_array($this->_pages)) {
340 foreach ($this->_pages as $tmp_page => $item) {
341 if (!empty($item['parent']) && $item['parent'] == $page) {
348 * Checks whether or not this module can be backed-up
350 * @return boolean true if this module can be backed-up, false otherwise
351 * @author Joel Kronenberg
353 function isBackupable() {
354 return is_file(AT_MODULE_PATH . $this->_directoryName.'/module_backup.php');
357 function createGroup($group_id) {
358 if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php')) {
359 require_once(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php');
360 $fn_name = basename($this->_directoryName) .'_create_group';
365 function deleteGroup($group_id) {
366 $fn_name = basename($this->_directoryName) .'_delete_group';
368 if (!function_exists($fn_name) && is_file(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php')) {
369 require_once(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php');
371 if (function_exists($fn_name)) {
376 function getGroupTool() {
377 if (!isset($this->_group_tool)) {
381 return $this->_group_tool;
384 function isGroupable() {
385 return is_file(AT_MODULE_PATH . $this->_directoryName.'/module_groups.php');
389 * Backup this module for a given course
391 * @param int $course_id ID of the course to backup
392 * @param object $zipfile a reference to a zipfile object
393 * @author Joel Kronenberg
395 function backup($course_id, &$zipfile) {
398 if (!isset($CSVExport)) {
399 require_once(AT_INCLUDE_PATH . 'classes/CSVExport.class.php');
400 $CSVExport = new CSVExport();
404 if ($this->isBackupable()) {
405 require(AT_MODULE_PATH . $this->_directoryName . '/module_backup.php');
407 foreach ($sql as $file_name => $table_sql) {
408 $content = $CSVExport->export($table_sql, $course_id);
410 $zipfile->add_file($content, $file_name . '.csv', $now);
416 foreach ($dirs as $dir => $path) {
417 $path = str_replace('?', $course_id, $path);
419 $zipfile->add_dir($path , $dir);
426 * Restores this module into the given course
428 * @param int $course_id ID of the course to restore into
429 * @param string $version version number of the ATutor installation used to make this backup
430 * @param string $import_dir the path to the import directory
431 * @author Joel Kronenberg
433 function restore($course_id, $version, $import_dir) {
435 if (!file_exists(AT_MODULE_PATH . $this->_directoryName.'/module_backup.php')) {
439 if (!isset($CSVImport)) {
440 require_once(AT_INCLUDE_PATH . 'classes/CSVImport.class.php');
441 $CSVImport = new CSVImport();
444 require(AT_MODULE_PATH . $this->_directoryName.'/module_backup.php');
447 foreach ($sql as $table_name => $table_sql) {
448 $CSVImport->import($table_name, $import_dir, $course_id, $version);
451 if ($this->_directoryName == '_core/content')
453 if (version_compare($version, '1.6.4', '<')) {
454 $this->convertContent164($course_id);
459 foreach ($dirs as $src => $dest) {
460 $dest = str_replace('?', $course_id, $dest);
461 copys($import_dir.$src, $dest);
467 * Delete this module's course content. If $groups is specified then it will
468 * delete all content for the groups specified.
470 * @param int $course_id ID of the course to delete
471 * @param array $groups Array of groups to delete
472 * @author Joel Kronenberg
474 function delete($course_id, $groups) {
475 if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module_delete.php')) {
476 require(AT_MODULE_PATH . $this->_directoryName.'/module_delete.php');
477 if (function_exists(basename($this->_directoryName).'_delete')) {
478 $fnctn = basename($this->_directoryName).'_delete';
483 foreach ($groups as $group_id) {
484 $this->deleteGroup($group_id);
490 * Enables the installed module
492 * @author Joel Kronenberg
497 $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_ENABLED.' WHERE dir_name="'.$this->_directoryName.'"';
498 $result = mysql_query($sql, $db);
502 * Sets the status to missing if the module dir doesn't exist.
504 * @param boolean $force whether or not to force the module to be missing (used for bundled extra modules upon upgrade)
505 * @author Joel Kronenberg
507 function setIsMissing($force = false) {
509 // if the directory doesn't exist then set the status to MISSING
510 if ($force || !is_dir(AT_MODULE_PATH . $this->_directoryName)) {
511 $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_MISSING.' WHERE dir_name="'.$this->_directoryName.'"';
512 $result = mysql_query($sql, $db);
517 * Disables the installed module
519 * @author Joel Kronenberg
524 // remove any privileges admins, students
525 if ($this->_privilege > 1) {
526 $sql = 'UPDATE '. TABLE_PREFIX . 'course_enrollment SET `privileges`=`privileges`-'.$this->_privilege.' WHERE `privileges` > 1 AND (`privileges` & '.$this->_privilege.')<>0';
527 $result = mysql_query($sql, $db);
530 if ($this->_admin_privilege > 1) {
531 $sql = 'UPDATE '. TABLE_PREFIX . 'admins SET `privileges`=`privileges`-'.$this->_admin_privilege.' WHERE `privileges` > 1 AND (`privileges` & '.$this->_admin_privilege.')<>0';
532 $result = mysql_query($sql, $db);
535 $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_DISABLED.' WHERE dir_name="'.$this->_directoryName.'"';
536 $result = mysql_query($sql, $db);
538 if (function_exists(basename($this->_directoryName).'_disable')) {
539 $fn_name = basename($this->_directoryName).'_disable';
545 * Installs the module
547 * @author Joel Kronenberg
552 // should check if this module is already installed...
554 if (file_exists(AT_MODULE_PATH . $this->_directoryName . '/module_install.php')) {
555 require(AT_MODULE_PATH . $this->_directoryName . '/module_install.php');
558 if (!$msg->containsErrors()) {
561 $sql = "SELECT MAX(`privilege`) AS `privilege`, MAX(admin_privilege) AS admin_privilege FROM ".TABLE_PREFIX."modules";
562 $result = mysql_query($sql, $db);
563 $row = mysql_fetch_assoc($result);
565 if (($_course_privilege === TRUE) || ((string) $_course_privilege == 'new')) {
566 $priv = $row['privilege'] * 2;
567 } else if ($_course_privilege == AT_PRIV_ADMIN) {
568 $priv = AT_PRIV_ADMIN;
573 if (($_admin_privilege === TRUE) || ((string) $_admin_privilege == 'new')) {
574 $admin_priv = $row['admin_privilege'] * 2;
576 $admin_priv = AT_ADMIN_PRIV_ADMIN;
579 if (isset($_cron_interval)) {
580 $_cron_interval = abs($_cron_interval);
585 $sql = 'INSERT INTO '. TABLE_PREFIX . 'modules VALUES ("'.$this->_directoryName.'", '.AT_MODULE_STATUS_DISABLED.', '.$priv.', '.$admin_priv.', '.$_cron_interval.', 0)';
586 mysql_query($sql, $db);
587 if (mysql_affected_rows($db) != 1) {
588 // in case this module has to be re-installed (because it was Missing)
589 $sql = 'UPDATE '. TABLE_PREFIX . 'modules SET status='.AT_MODULE_STATUS_DISABLED.' WHERE dir_name="'.$this->_directoryName.'"';
590 mysql_query($sql, $db);
596 * Uninstalls the module
598 * @author Cindy Qi Li
600 function uninstall($del_data='') {
603 if (file_exists(AT_MODULE_PATH . $this->_directoryName . '/module_uninstall.php') && $del_data == 1)
605 require(AT_MODULE_PATH . $this->_directoryName . '/module_uninstall.php');
608 if (!$msg->containsErrors())
610 require_once(AT_INCLUDE_PATH.'lib/filemanager.inc.php');
612 if (!clr_dir(AT_MODULE_PATH . $this->_directoryName))
613 $msg->addError(array('MODULE_UNINSTALL', '<li>'.AT_MODULE_PATH . $this->_directoryName.' can not be removed. Please manually remove it.</li>'));
616 if (!$msg->containsErrors())
620 $sql = "DELETE FROM ". TABLE_PREFIX . "modules WHERE dir_name = '".$this->_directoryName."'";
621 mysql_query($sql, $db);
624 if ($msg->containsErrors())
628 $sql = "UPDATE ". TABLE_PREFIX . "modules SET status=".AT_MODULE_STATUS_PARTIALLY_UNINSTALLED." WHERE dir_name='".$this->_directoryName."'";
629 mysql_query($sql, $db);
633 function getStudentTools() {
634 if (!isset($this->_student_tool)) {
638 return $this->_student_tool;
643 if ( ($this->_cron_last_run + ($this->_cron_interval * 60)) < time()) {
644 if (is_file(AT_MODULE_PATH . $this->_directoryName.'/module_cron.php')) {
645 require(AT_MODULE_PATH . $this->_directoryName.'/module_cron.php');
646 if (function_exists(basename($this->_directoryName).'_cron')) {
647 $fnctn = basename($this->_directoryName).'_cron';
651 $this->updateCronLastRun();
655 // i'm private! update the last time the cron was run
656 function updateCronLastRun() {
659 $sql = "UPDATE ".TABLE_PREFIX."modules SET cron_last_run=".time()." WHERE dir_name='$this->_directoryName'";
660 mysql_query($sql, $db);
664 private function convertContent164($course_id) {
667 /* convert all content nodes to the IMS standard. (adds null nodes for all top pages) */
668 /* 1. Convert db to a tree */
669 $sql = 'SELECT * FROM '.TABLE_PREFIX.'content where course_id='.$course_id;
671 $result = mysql_query($sql, $db);
672 $content_array = array();
674 while ($row = mysql_fetch_assoc($result)){
675 $content_array[$row['content_parent_id']][$row['ordering']] = $row['content_id'];
677 $tree = $this->buildTree($content_array[0], $content_array);
679 /* 2. Restructure the tree */
680 $tree = $this->rebuild($tree);
682 /* 3. Update the Db based on this new tree */
683 $this->reconstruct($tree, '', 0, TABLE_PREFIX);
687 * Construct a tree based on table entries
688 * @param array current node, (current parent)
689 * @param mixed a set of parents, where each parents is in the format of [parent]=>children
690 * should remain the same throughout the recursion.
691 * @return A tree structure representation of the content entries.
692 * @author Harris Wong
694 private function buildTree($current, $content_array){
696 foreach($current as $order=>$content_id){
698 if (isset($content_array[$content_id])){
699 $wrapper[$content_id] = $this->buildTree($content_array[$content_id], $content_array);
704 $folder['order_'.$order] = $wrapper;
707 $folder['order_'.$order] = $content_id;
715 * Transverse the content tree structure, and reconstruct it with the IMS spec.
716 * This tree has the structure of [order=>array(id)], so the first layer is its order, second is the id
717 * if param merge is true, if node!=null, merge it to top layer, and + offset to all others
718 * @param mixed Tree from the buildTree() function, or sub-tree
719 * @param mixed the current tree.
720 * @return A new content tree that meets the IMS specification.
721 * @author Harris Wong
723 private function rebuild($tree, $node=''){
726 if (!is_array($tree)){
730 $tree['order_0'] = $node;
733 //go through the tree
734 foreach($tree as $k=>$v){
735 if (preg_match('/order\_([\d]+)/', $k, $match)==1){
736 //if this is the order layer
737 $folder['order_'.($match[1]+$order_offset)] = $this->rebuild($v);
739 //if this is the content layer
741 $folder[$k] = $this->rebuild($v, $k);
749 * Transverse the tree and update/insert entries based on the updated structure.
750 * @param array The tree from rebuild(), and the subtree from the recursion.
751 * @param int the ordering of this subtree respect to its parent.
752 * @param int parent content id
753 * @return null (nothing to return, it updates the db only)
755 private function reconstruct($tree, $order, $content_parent_id, $table_prefix){
759 if (!is_array($tree)){
760 $sql = 'UPDATE '.$table_prefix."content SET ordering=$order, content_parent_id=$content_parent_id WHERE content_id=$tree";
761 if (!mysql_query($sql, $db)){
767 foreach ($tree as $k=>$v){
768 if (preg_match('/order\_([\d]+)/', $k, $match)==1){
770 $this->reconstruct($v, $match[1], $content_parent_id, $table_prefix); //inherit the previous layer id
772 //content folder layer
773 $sql = 'SELECT * FROM '.$table_prefix."content WHERE content_id=$k";
774 $result = mysql_query($sql, $db);
775 $old_content_row = mysql_fetch_assoc($result);
776 $sql = 'INSERT INTO '.$table_prefix.'content (course_id, content_parent_id, ordering, last_modified, revision, formatting, release_date, keywords, content_path, title, use_customized_head, allow_test_export, content_type) VALUES ('
777 .$old_content_row['course_id'] . ', '
778 .$content_parent_id . ', '
780 .'\''. $old_content_row['last_modified'] . '\', '
781 .$old_content_row['revision'] . ', '
782 .$old_content_row['formatting'] . ', '
783 .'\''. $old_content_row['release_date'] . '\', '
784 .'\''. $old_content_row['keywords'] . '\', '
785 .'\''. $old_content_row['content_path'] . '\', '
786 .'\''. $old_content_row['title'] . '\', '
787 .$old_content_row['use_customized_head'] . ', '
788 .$old_content_row['allow_test_export'] . ', '
791 if (mysql_query($sql, $db)){
792 $folder_id = mysql_insert_id();
793 $this->reconstruct($v, '', $folder_id, $table_prefix);