. namespace core_course; use core\{clock, di}; use core\context; use core\context\course as context_course; use core\context\coursecat as context_coursecat; use core\context\system as context_system; use core\exception\coding_exception; use core\exception\moodle_exception; use core_course_category; use restore_dbops; use stdClass; /** * This class pertains to course requests and contains methods associated with * create, approving, and removing course requests. * * Please note we do not allow embedded images here because there is no context * to store them with proper access control. * * @package core_course * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 * * @property-read int $id * @property-read string $fullname * @property-read string $shortname * @property-read string $summary * @property-read int $summaryformat * @property-read int $summarytrust * @property-read string $reason * @property-read int $requester */ class course_request { /** * This is the stdClass that stores the properties for the course request * and is externally accessed through the __get magic method * @var stdClass */ protected $properties; /** * An array of options for the summary editor used by course request forms. * * This is initially set by {@see summary_editor_options()} * @var array */ protected static $summaryeditoroptions; /** * Static function to prepare the summary editor for working with a course request. * * @param null|stdClass $data Optional, an object containing the default values * for the form, these may be modified when preparing the * editor so this should be called before creating the form * @return stdClass An object that can be used to set the default values for * an mforms form */ public static function prepare($data = null) { if ($data === null) { $data = new stdClass(); } $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options()); return $data; } /** * Static function to create a new course request when passed an array of properties * for it. * * This function also handles saving any files that may have been used in the editor * * @param stdClass $data * @return course_request The newly created course request */ public static function create($data) { global $USER, $DB, $CFG; $data->requester = $USER->id; // Setting the default category if none set. if (empty($data->category) || !empty($CFG->lockrequestcategory)) { $data->category = $CFG->defaultrequestcategory; } // Summary is a required field so copy the text over. $data->summary = $data->summary_editor['text']; $data->summaryformat = $data->summary_editor['format']; $data->id = $DB->insert_record('course_request', $data); // Create a new course_request object and return it. $request = new course_request($data); // Notify the admin if required. if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) { $category = \core_course_category::get($data->category); $a = new stdClass(); $a->link = "$CFG->wwwroot/course/pending.php"; $a->user = fullname($USER); $a->shortname = format_string($data->shortname, true, ['context' => $category->get_context()]); $a->fullname = format_string($data->fullname, true, ['context' => $category->get_context()]); $a->category = $category->get_formatted_name(); $a->reason = format_text($data->reason, FORMAT_PLAIN); $subject = get_string('courserequest'); $message = get_string('courserequestnotifyemail', 'admin', $a); foreach ($users as $user) { $request->notify($user, $USER, 'courserequested', $subject, $message); } } return $request; } /** * Returns an array of options to use with a summary editor * * @uses course_request::$summaryeditoroptions * @return array An array of options to use with the editor */ public static function summary_editor_options() { global $CFG; if (self::$summaryeditoroptions === null) { self::$summaryeditoroptions = ['maxfiles' => 0, 'maxbytes' => 0]; } return self::$summaryeditoroptions; } /** * Loads the properties for this course request object. Id is required and if * only id is provided then we load the rest of the properties from the database * * @param stdClass|int $properties Either an object containing properties * or the course_request id to load */ public function __construct($properties) { global $DB; if (empty($properties->id)) { if (empty($properties)) { throw new coding_exception('You must provide a course request id when creating a course_request object'); } $id = $properties; $properties = new stdClass(); $properties->id = (int)$id; unset($id); } if (empty($properties->requester)) { if (!($this->properties = $DB->get_record('course_request', ['id' => $properties->id]))) { throw new moodle_exception('unknowncourserequest'); } } else { $this->properties = $properties; } $this->properties->collision = null; } /** * Returns the requested property * * @param string $key * @return mixed */ public function __get($key) { return $this->properties->$key; } /** * Override this to ensure empty($request->blah) calls return a reliable answer... * * This is required because we define the __get method * * @param mixed $key * @return bool True is it not empty, false otherwise */ public function __isset($key) { return (!empty($this->properties->$key)); } /** * Returns the user who requested this course. * * Uses a static var to cache the results and cut down the number of db queries * * @return stdClass The user who requested the course */ public function get_requester() { global $DB; static $requesters = []; if (!array_key_exists($this->properties->requester, $requesters)) { $requesters[$this->properties->requester] = $DB->get_record('user', ['id' => $this->properties->requester]); } return $requesters[$this->properties->requester]; } /** * Checks that the shortname used by the course does not conflict with any other * courses that exist * * @param string|null $shortnamemark The string to append to the requests shortname * should a conflict be found * @return bool true is there is a conflict, false otherwise */ public function check_shortname_collision($shortnamemark = '[*]') { global $DB; if ($this->properties->collision !== null) { return $this->properties->collision; } if (empty($this->properties->shortname)) { debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER); $this->properties->collision = false; } else if ($DB->record_exists('course', ['shortname' => $this->properties->shortname])) { if (!empty($shortnamemark)) { $this->properties->shortname .= ' ' . $shortnamemark; } $this->properties->collision = true; } else { $this->properties->collision = false; } return $this->properties->collision; } /** * Checks user capability to approve a requested course * * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'. * * @return bool */ public function can_approve() { global $CFG; $category = null; if ($this->properties->category) { $category = core_course_category::get($this->properties->category, IGNORE_MISSING); } else if ($CFG->defaultrequestcategory) { $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING); } if ($category) { return has_capability('moodle/site:approvecourse', $category->get_context()); } // We can not determine the context where the course should be created. The approver should have // both capabilities to approve courses and change course category in the system context. return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance()); } /** * Returns the category where this course request should be created * * Note that we don't check here that user has a capability to view * hidden categories if he has capabilities 'moodle/site:approvecourse' and * 'moodle/course:changecategory' * * @return core_course_category */ public function get_category() { global $CFG; if ( $this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING)) ) { return $category; } else if ( $CFG->defaultrequestcategory && ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING)) ) { return $category; } else { return core_course_category::get_default(); } } /** * This function approves the request turning it into a course * * This function converts the course request into a course, at the same time * transferring any files used in the summary to the new course and then removing * the course request and the files associated with it. * * @return int The id of the course that was created from this request */ public function approve() { global $CFG, $DB, $USER; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); $user = $DB->get_record('user', ['id' => $this->properties->requester, 'deleted' => 0], '*', MUST_EXIST); $courseconfig = get_config('moodlecourse'); // Transfer appropriate settings. $data = clone($this->properties); unset($data->id); unset($data->reason); unset($data->requester); // Set category. $category = $this->get_category(); $data->category = $category->id; // Set misc settings. $data->requested = 1; // Apply course default settings. $data->format = $courseconfig->format; $data->newsitems = $courseconfig->newsitems; $data->showgrades = $courseconfig->showgrades; $data->showreports = $courseconfig->showreports; $data->showactivitydates = $courseconfig->showactivitydates; $data->maxbytes = $courseconfig->maxbytes; $data->groupmode = $courseconfig->groupmode; $data->groupmodeforce = $courseconfig->groupmodeforce; $data->visible = $courseconfig->visible; $data->visibleold = $data->visible; $data->lang = $courseconfig->lang; $data->enablecompletion = $courseconfig->enablecompletion; $data->numsections = $courseconfig->numsections; $data->startdate = usergetmidnight(di::get(clock::class)->time()); if ($courseconfig->courseenddateenabled) { $data->enddate = $data->startdate + $courseconfig->courseduration; } [$data->fullname, $data->shortname] = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname); $course = create_course($data); $context = context_course::instance($course->id, MUST_EXIST); // Add enrol instances. if (!$DB->record_exists('enrol', ['courseid' => $course->id, 'enrol' => 'manual'])) { if ($manual = enrol_get_plugin('manual')) { $manual->add_default_instance($course); } } // Enrol the requester as teacher if necessary. if ( !empty($CFG->creatornewroleid) && !is_viewing($context, $user, 'moodle/role:assign') && !is_enrolled($context, $user, 'moodle/role:assign') ) { enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid); } $this->delete(); $a = new stdClass(); $a->name = format_string($course->fullname, true, ['context' => $context]); $a->url = course_get_url($course); $usernameplaceholders = \core\user::get_name_placeholders($user); foreach ($usernameplaceholders as $field => $value) { $a->{$field} = $value; } $this->notify( touser: $user, fromuser: $USER, name: 'courserequestapproved', subject: get_string('courseapprovedsubject'), message: get_string('courseapprovedemail2', 'moodle', $a), courseid: $course->id, ); return $course->id; } /** * Reject a course request * * This function rejects a course request, emailing the requesting user the * provided notice and then removing the request from the database * * @param string $notice The message to display to the user */ public function reject($notice) { global $USER, $DB; $user = $DB->get_record('user', ['id' => $this->properties->requester], '*', MUST_EXIST); $this->notify( touser: $user, fromuser: $USER, name: 'courserequestrejected', subject: get_string('courserejectsubject'), message: get_string('courserejectemail', 'moodle', $notice), ); $this->delete(); } /** * Deletes the course request and any associated files */ public function delete() { global $DB; $DB->delete_records('course_request', ['id' => $this->properties->id]); } /** * Send a message from one user to another using events_trigger * * @param object $touser * @param object $fromuser * @param string $name * @param string $subject * @param string $message * @param int|null $courseid */ protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) { $eventdata = new \core\message\message(); $eventdata->courseid = empty($courseid) ? SITEID : $courseid; $eventdata->component = 'moodle'; $eventdata->name = $name; $eventdata->userfrom = $fromuser; $eventdata->userto = $touser; $eventdata->subject = $subject; $eventdata->fullmessage = $message; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->smallmessage = ''; $eventdata->notification = 1; message_send($eventdata); } /** * Checks if current user can request a course in this context * * @param context $context * @return bool */ public static function can_request(context $context) { global $CFG; if (empty($CFG->enablecourserequests)) { return false; } if (has_capability('moodle/course:create', $context)) { return false; } if ($context instanceof context_system) { $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING); return $defaultcontext && has_capability('moodle/course:request', $defaultcontext); } else if ($context instanceof context_coursecat) { if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) { return has_capability('moodle/course:request', $context); } } return false; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(course_request::class, \course_request::class);