. /** * General plugin functions. * * @package local * @subpackage ltiprovider * @copyright 2011 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die; require_once($CFG->dirroot.'/local/ltiprovider/ims-blti/blti_util.php'); require_once($CFG->dirroot.'/local/ltiprovider/locallib.php'); use moodle\local\ltiprovider as ltiprovider; /** * Display the LTI settings in the course settings block * For 2.3 and onwards * * @param settings_navigation $nav The settings navigatin object * @param stdclass $context Course context */ function local_ltiprovider_extend_settings_navigation(settings_navigation $nav, $context) { if ($context->contextlevel >= CONTEXT_COURSE and ($branch = $nav->get('courseadmin')) and has_capability('local/ltiprovider:view', $context)) { $ltiurl = new moodle_url('/local/ltiprovider/index.php', array('courseid' => $context->instanceid)); $branch->add(get_string('pluginname', 'local_ltiprovider'), $ltiurl, $nav::TYPE_CONTAINER, null, 'ltiprovider'.$context->instanceid); } } /** * Change the navigation block and bar only for external users * Force course or activity navigation and modify CSS also * Please note that this function is only called in pages where the navigation block is present * * @global moodle_user $USER * @global moodle_database $DB * @param navigation_node $nav Current navigation object */ function local_ltiprovider_extend_navigation ($nav) { global $CFG, $USER, $PAGE, $SESSION, $ME; if (isset($USER) and isset($USER->auth) and strpos($USER->username, 'ltiprovider') === 0) { // Force course or activity navigation. if (isset($SESSION->ltiprovider) and $SESSION->ltiprovider->forcenavigation) { $context = $SESSION->ltiprovider->context; $urltogo = ''; if ($context->contextlevel == CONTEXT_COURSE and $PAGE->course->id != $SESSION->ltiprovider->courseid) { $urltogo = new moodle_url('/course/view.php', array('id' => $SESSION->ltiprovider->courseid)); } else if ($context->contextlevel == CONTEXT_MODULE and $PAGE->context->id != $context->id) { $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); $urltogo = new moodle_url('/mod/'.$cm->modname.'/view.php', array('id' => $cm->id)); } // Special case, user policy, we don't have to do nothing to avoid infinites loops. if (strpos($ME, 'user/policy.php')) { return; } if ($urltogo) { local_ltiprovider_call_hook("navigation", $nav); if (!$PAGE->requires->is_head_done()) { $PAGE->set_state($PAGE::STATE_IN_BODY); } redirect($urltogo); } } // Delete all the navigation nodes except the course one. if ($coursenode = $nav->find($PAGE->course->id, $nav::TYPE_COURSE)) { foreach (array('myprofile', 'users', 'site', 'home', 'myhome', 'mycourses', 'courses', '1') as $nodekey) { if ($node = $nav->get($nodekey)) { $node->remove(); } } $nav->children->add($coursenode); } // Custom CSS. if (isset($SESSION->ltiprovider) and !$PAGE->requires->is_head_done()) { $PAGE->requires->css(new moodle_url('/local/ltiprovider/styles.php', array('id' => $SESSION->ltiprovider->id))); } elseif (isset($SESSION->ltiprovider) && isset($SESSION->ltiprovider->id)) { $url = new moodle_url('/local/ltiprovider/styles.js.php', array('id' => $SESSION->ltiprovider->id, 'rand' => rand(0, 1000))); $PAGE->requires->js($url); } local_ltiprovider_call_hook("navigation", $nav); } } /** * Add new tool. * * @param object $tool * @return int */ function local_ltiprovider_add_tool($tool) { global $DB; if (!isset($tool->disabled)) { $tool->disabled = 0; } if (!isset($tool->timecreated)) { $tool->timecreated = time(); } if (!isset($tool->timemodified)) { $tool->timemodified = $tool->timecreated; } if (!isset($tool->sendgrades)) { $tool->sendgrades = 0; } if (!isset($tool->forcenavigation)) { $tool->forcenavigation = 0; } if (!isset($tool->enrolinst)) { $tool->enrolinst = 0; } if (!isset($tool->enrollearn)) { $tool->enrollearn = 0; } if (!isset($tool->hidepageheader)) { $tool->hidepageheader = 0; } if (!isset($tool->hidepagefooter)) { $tool->hidepagefooter = 0; } if (!isset($tool->hideleftblocks)) { $tool->hideleftblocks = 0; } if (!isset($tool->hiderightblocks)) { $tool->hiderightblocks = 0; } if (!isset($tool->syncmembers)) { $tool->syncmembers = 0; } $tool->id = $DB->insert_record('local_ltiprovider', $tool); local_ltiprovider_call_hook('save_settings', $tool); return $tool->id; } /** * Update existing tool. * @param object $tool * @return void */ function local_ltiprovider_update_tool($tool) { global $DB; $tool->timemodified = time(); if (!isset($tool->sendgrades)) { $tool->sendgrades = 0; } if (!isset($tool->forcenavigation)) { $tool->forcenavigation = 0; } if (!isset($tool->enrolinst)) { $tool->enrolinst = 0; } if (!isset($tool->enrollearn)) { $tool->enrollearn = 0; } if (!isset($tool->hidepageheader)) { $tool->hidepageheader = 0; } if (!isset($tool->hidepagefooter)) { $tool->hidepagefooter = 0; } if (!isset($tool->hideleftblocks)) { $tool->hideleftblocks = 0; } if (!isset($tool->hiderightblocks)) { $tool->hiderightblocks = 0; } if (!isset($tool->syncmembers)) { $tool->syncmembers = 0; } local_ltiprovider_call_hook('save_settings', $tool); $DB->update_record('local_ltiprovider', $tool); } /** * Delete tool. * @param object $tool * @return void */ function local_ltiprovider_delete_tool($tool) { global $DB; $DB->delete_records('local_ltiprovider_user', array('toolid' => $tool->id)); $DB->delete_records('local_ltiprovider', array('id' => $tool->id)); } /** * Checks if a course linked to a tool is missing, is so, delete the lti entries * @param stdclass $tool Tool record * @return bool True if the course was missing */ function local_ltiprovider_check_missing_course($tool) { global $DB; if (! $course = $DB->get_record('course', array('id' => $tool->courseid))) { $DB->delete_records('local_ltiprovider', array('courseid' => $tool->courseid)); $DB->delete_records('local_ltiprovider_user', array('toolid' => $tool->id)); mtrace("Tool: $tool->id deleted (courseid: $tool->courseid missing)"); return true; } return false; } /** * Cron function for sync grades * @return void */ function local_ltiprovider_cron() { global $DB, $CFG; require_once($CFG->dirroot."/local/ltiprovider/locallib.php"); require_once($CFG->dirroot."/local/ltiprovider/ims-blti/OAuth.php"); require_once($CFG->dirroot."/local/ltiprovider/ims-blti/OAuthBody.php"); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/grade/querylib.php'); // TODO - Add a global setting for this $synctime = 60*60; // Every 1 hour grades are sync $timenow = time(); mtrace('Running cron for ltiprovider'); mtrace('Deleting LTI tools assigned to deleted courses'); if ($tools = $DB->get_records('local_ltiprovider')) { foreach ($tools as $tool) { local_ltiprovider_check_missing_course($tool); } } // Grades service. if ($tools = $DB->get_records_select('local_ltiprovider', 'disabled = ? AND sendgrades = ?', array(0, 1))) { foreach ($tools as $tool) { if ($tool->lastsync + $synctime < $timenow) { mtrace(" Starting sync tool for grades id $tool->id course id $tool->courseid"); if ($tool->requirecompletion) { mtrace(" Grades require activity or course completion"); } $user_count = 0; $send_count = 0; $error_count = 0; $completion = new completion_info(get_course($tool->courseid)); if ($users = $DB->get_records('local_ltiprovider_user', array('toolid' => $tool->id))) { foreach ($users as $user) { $data = array( 'tool' => $tool, 'user' => $user, ); local_ltiprovider_call_hook('grades', (object) $data); $user_count = $user_count + 1; // This can happen is the sync process has an unexpected error if ( strlen($user->serviceurl) < 1 ) { mtrace(" Empty serviceurl"); continue; } if ( strlen($user->sourceid) < 1 ) { mtrace(" Empty sourceid"); continue; } if ($user->lastsync > $tool->lastsync) { mtrace(" Skipping user {$user->id} due to recent sync"); continue; } $grade = false; if ($context = $DB->get_record('context', array('id' => $tool->contextid))) { if ($context->contextlevel == CONTEXT_COURSE) { if ($tool->requirecompletion and !$completion->is_course_complete($user->userid)) { mtrace(" Skipping user $user->userid since he didn't complete the course"); continue; } if ($tool->sendcompletion) { $grade = $completion->is_course_complete($user->userid) ? 1 : 0; $grademax = 1; } else if ($grade = grade_get_course_grade($user->userid, $tool->courseid)) { $grademax = floatval($grade->item->grademax); $grade = $grade->grade; } } else if ($context->contextlevel == CONTEXT_MODULE) { $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); if ($tool->requirecompletion) { $data = $completion->get_data($cm, false, $user->userid); if ($data->completionstate != COMPLETION_COMPLETE_PASS and $data->completionstate != COMPLETION_COMPLETE) { mtrace(" Skipping user $user->userid since he didn't complete the activity"); continue; } } if ($tool->sendcompletion) { $data = $completion->get_data($cm, false, $user->userid); if ($data->completionstate == COMPLETION_COMPLETE_PASS || $data->completionstate == COMPLETION_COMPLETE || $data->completionstate == COMPLETION_COMPLETE_FAIL) { $grade = 1; } else { $grade = 0; } $grademax = 1; } else { $grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance, $user->userid); if (empty($grades->items[0]->grades)) { $grade = false; } else { $grade = reset($grades->items[0]->grades); if (!empty($grade->item)) { $grademax = floatval($grade->item->grademax); } else { $grademax = floatval($grades->items[0]->grademax); } $grade = $grade->grade; } } } if ( $grade === false || $grade === NULL || strlen($grade) < 1) { mtrace(" Invalid grade $grade"); continue; } // No need to be dividing by zero if ( $grademax == 0.0 ) $grademax = 100.0; // TODO: Make lastgrade should be float or string - but it is integer so we truncate // TODO: Then remove those intval() calls // Don't double send if ( intval($grade) == $user->lastgrade ) { mtrace(" Skipping, last grade send is equal to current grade"); continue; } // We sync with the external system only when the new grade differs with the previous one // TODO - Global setting for check this if ($grade >= 0 and $grade <= $grademax) { $float_grade = $grade / $grademax; $body = local_ltiprovider_create_service_body($user->sourceid, $float_grade); try { $response = ltiprovider\sendOAuthBodyPOST('POST', $user->serviceurl, $user->consumerkey, $user->consumersecret, 'application/xml', $body); } catch (Exception $e) { mtrace(" ".$e->getMessage()); $error_count = $error_count + 1; continue; } // TODO - Check for errors in $retval in a correct way (parsing xml) if (strpos(strtolower($response), 'success') !== false) { $DB->set_field('local_ltiprovider_user', 'lastsync', $timenow, array('id' => $user->id)); $DB->set_field('local_ltiprovider_user', 'lastgrade', intval($grade), array('id' => $user->id)); mtrace(" User grade sent to remote system. userid: $user->userid grade: $float_grade"); $send_count = $send_count + 1; } else { mtrace(" User grade send failed. userid: $user->userid grade: $float_grade: " . $response); $error_count = $error_count + 1; } } else { mtrace(" User grade for user $user->userid out of range: grade = ".$grade); $error_count = $error_count + 1; } } else { mtrace(" Invalid context: contextid = ".$tool->contextid); } } } mtrace(" Completed sync tool id $tool->id course id $tool->courseid users=$user_count sent=$send_count errors=$error_count"); $DB->set_field('local_ltiprovider', 'lastsync', $timenow, array('id' => $tool->id)); } } } $timenow = time(); // Automatic course restaurations. if ($croncourses = get_config('local_ltiprovider', 'croncourses')) { $croncourses = unserialize($croncourses); if (is_array($croncourses)) { mtrace('Starting restauration of pending courses'); foreach ($croncourses as $key => $course) { mtrace('Starting restoration of ' . $key); // We limit the backups to 1 hour, then retry. if ($course->restorestart and ($timenow < $course->restorestart + 3600)) { mtrace('Skipping restoration in process for: ' . $key); continue; } $course->restorestart = time(); $croncourses[$key] = $course; $croncoursessafe = serialize($croncourses); set_config('croncourses', $croncoursessafe, 'local_ltiprovider'); if ($destinationcourse = $DB->get_record('course', array('id' => $course->destinationid))) { // Duplicate course + users. local_ltiprovider_duplicate_course($course->id, $destinationcourse, 1, $options = array(array('name' => 'users', 'value' => 1)), $course->userrestoringid, $course->context); mtrace('Restoration for ' .$key. ' finished'); } else { mtrace('Restoration for ' .$key. ' finished (destination course not exists)'); } unset($croncourses[$key]); $croncoursessafe = serialize($croncourses); set_config('croncourses', $croncoursessafe, 'local_ltiprovider'); } } } // Membership service. $timenow = time(); $userphotos = array(); if ($tools = $DB->get_records('local_ltiprovider', array('disabled' => 0, 'syncmembers' => 1))) { mtrace('Starting sync of member using the memberships service'); $consumers = array(); foreach ($tools as $tool) { $lastsync = get_config('local_ltiprovider', 'membershipslastsync-' . $tool->id); if (!$lastsync) { $lastsync = 0; } if ($lastsync + $tool->syncperiod < $timenow) { $ret = local_ltiprovider_membership_service($tool, $timenow, $userphotos, $consumers); $userphotos = $ret['userphotos']; $consumers = $ret['consumers']; } else { $last = format_time((time() - $lastsync)); mtrace("Tool $tool->id synchronized $last ago"); } mtrace('Finished sync of member using the memberships service'); } } local_ltiprovider_membership_service_update_userphotos($userphotos); } /** * Call a hook present in a subplugin * * @param string $hookname The hookname (function without franken style prefix) * @param object $data Object containing data to be used by the hook function * @return bool Allways false */ function local_ltiprovider_call_hook($hookname, $data) { $plugins = get_plugin_list_with_function('ltiproviderextension', $hookname); if (!empty($plugins)) { foreach ($plugins as $plugin) { call_user_func($plugin, $data); } } return false; }