���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/classes.tar
���ѧ٧ѧ�
privacy/provider.php 0000644 00000002773 15151174071 0010577 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for block_activity_modules. * * @package atto_align * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace atto_align\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for atto_align implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } token_form.php 0000644 00000010460 15151174071 0007423 0 ustar 00 <?php // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides the {@see \core_webservice\token_form} class. * * @package core_webservice * @category admin * @copyright 2020 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_webservice; use core_user; /** * Form to create and edit a web service token. * * Tokens allow users call external functions provided by selected web services. They can optionally have IP restriction * and date validity defined. * * @copyright 2010 Jerome Mouneyrac <jerome@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class token_form extends \moodleform { /** * Defines the form fields. */ public function definition() { global $DB; $mform = $this->_form; $data = $this->_customdata; $mform->addElement('header', 'token', get_string('token', 'webservice')); // User selector. $attributes = [ 'multiple' => false, 'ajax' => 'core_user/form_user_selector', 'valuehtmlcallback' => function($userid) { global $OUTPUT; $context = \context_system::instance(); $fields = \core_user\fields::for_name()->with_identity($context, false); $record = core_user::get_user($userid, 'id ' . $fields->get_sql()->selects, MUST_EXIST); $user = (object)[ 'id' => $record->id, 'fullname' => fullname($record, has_capability('moodle/site:viewfullnames', $context)), 'extrafields' => [], ]; foreach ($fields->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $extrafield) { $user->extrafields[] = (object)[ 'name' => $extrafield, 'value' => s($record->$extrafield) ]; } return $OUTPUT->render_from_template('core_user/form_user_selector_suggestion', $user); }, ]; $mform->addElement('autocomplete', 'user', get_string('user'), [], $attributes); $mform->addRule('user', get_string('required'), 'required', null, 'client'); // Service selector. $options = $DB->get_records_menu('external_services', null, '', 'id, name'); $mform->addElement('select', 'service', get_string('service', 'webservice'), $options); $mform->addRule('service', get_string('required'), 'required', null, 'client'); $mform->setType('service', PARAM_INT); $mform->addElement('text', 'iprestriction', get_string('iprestriction', 'webservice')); $mform->setType('iprestriction', PARAM_RAW_TRIMMED); $mform->addElement('date_selector', 'validuntil', get_string('validuntil', 'webservice'), array('optional' => true)); $mform->setType('validuntil', PARAM_INT); $mform->addElement('hidden', 'action'); $mform->setType('action', PARAM_ALPHANUMEXT); $this->add_action_buttons(true); $this->set_data($data); } /** * Validate the submitted data. * * @param array $data Submitted data. * @param array $files Submitted files. * @return array Validation errors. */ public function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); if ($DB->get_field('user', 'suspended', ['id' => $data['user']], MUST_EXIST)) { $errors['user'] = get_string('suspended', 'core') . ' - ' . get_string('forbiddenwsuser', 'core_webservice'); } return $errors; } } token_filter.php 0000644 00000007265 15151174071 0007756 0 ustar 00 <?php // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides the {@see core_webservice\token_filter} class. * * @package core_webservice * @copyright 2020 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_webservice; use moodleform; /** * Form allowing to filter displayed tokens. * * @copyright 2020 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class token_filter extends moodleform { /** * Defines the form fields. */ public function definition() { global $DB; $mform = $this->_form; $presetdata = $this->_customdata; $mform->addElement('header', 'tokenfilter', get_string('tokenfilter', 'webservice')); if (empty($presetdata->token) && empty($presetdata->users) && empty($presetdata->services)) { $mform->setExpanded('tokenfilter', false); } else { $mform->setExpanded('tokenfilter', true); } // Token. $mform->addElement('text', 'token', get_string('token', 'core_webservice'), ['size' => 32]); $mform->setType('token', PARAM_ALPHANUM); // User selector. $attributes = [ 'multiple' => true, 'ajax' => 'core_user/form_user_selector', 'valuehtmlcallback' => function($userid) { global $DB, $OUTPUT; $context = \context_system::instance(); $fields = \core_user\fields::for_name()->with_identity($context, false); $record = \core_user::get_user($userid, 'id' . $fields->get_sql()->selects, MUST_EXIST); $user = (object)[ 'id' => $record->id, 'fullname' => fullname($record, has_capability('moodle/site:viewfullnames', $context)), 'extrafields' => [], ]; foreach ($fields->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $extrafield) { $user->extrafields[] = (object)[ 'name' => $extrafield, 'value' => s($record->$extrafield) ]; } return $OUTPUT->render_from_template('core_user/form_user_selector_suggestion', $user); }, ]; $mform->addElement('autocomplete', 'users', get_string('user'), [], $attributes); // Service selector. $options = $DB->get_records_menu('external_services', null, '', 'id, name'); $attributes = [ 'multiple' => true, ]; $mform->addElement('autocomplete', 'services', get_string('service', 'webservice'), $options, $attributes); // Action buttons. $mform->addGroup([ $mform->createElement('submit', 'submitbutton', get_string('tokenfiltersubmit', 'core_webservice')), $mform->createElement('submit', 'resetbutton', get_string('tokenfilterreset', 'core_webservice'), [], false), ], 'actionbuttons', '', ' ', false); } } token_table.php 0000644 00000027111 15151174071 0007550 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the class used for the displaying the tokens table. * * @package core_webservice * @copyright 2017 John Okely <john@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_webservice; defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/tablelib.php'); require_once($CFG->dirroot . '/webservice/lib.php'); require_once($CFG->dirroot . '/user/lib.php'); /** * Class for the displaying the participants table. * * @package core_webservice * @copyright 2017 John Okely <john@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class token_table extends \table_sql { /** * @var bool $showalltokens Whether or not the user is able to see all tokens. */ protected $showalltokens; /** @var bool $hasviewfullnames Does the user have the viewfullnames capability. */ protected $hasviewfullnames; /** @var array */ protected $userextrafields; /** @var object */ protected $filterdata; /** * Sets up the table. * * @param int $id The id of the table * @param object $filterdata The data submitted by the {@see token_filter}. */ public function __construct($id, $filterdata = null) { parent::__construct($id); // Get the context. $context = \context_system::instance(); // Can we see tokens created by all users? $this->showalltokens = has_capability('moodle/webservice:managealltokens', $context); $this->hasviewfullnames = has_capability('moodle/site:viewfullnames', $context); // List of user identity fields. $this->userextrafields = \core_user\fields::get_identity_fields(\context_system::instance(), false); // Filter form values. $this->filterdata = $filterdata; // Define the headers and columns. $headers = []; $columns = []; $headers[] = get_string('token', 'webservice'); $columns[] = 'token'; $headers[] = get_string('user'); $columns[] = 'fullname'; $headers[] = get_string('service', 'webservice'); $columns[] = 'servicename'; $headers[] = get_string('iprestriction', 'webservice'); $columns[] = 'iprestriction'; $headers[] = get_string('validuntil', 'webservice'); $columns[] = 'validuntil'; if ($this->showalltokens) { // Only need to show creator if you can see tokens created by other people. $headers[] = get_string('tokencreator', 'webservice'); $columns[] = 'creatorlastname'; // So we can have semi-useful sorting. Table SQL doesn't two fullname collumns. } $headers[] = get_string('operation', 'webservice'); $columns[] = 'operation'; $this->define_columns($columns); $this->define_headers($headers); $this->no_sorting('operation'); $this->no_sorting('token'); $this->no_sorting('iprestriction'); $this->set_attribute('id', $id); } /** * Generate the operation column. * * @param \stdClass $data Data for the current row * @return string Content for the column */ public function col_operation($data) { $tokenpageurl = new \moodle_url( "/admin/webservice/tokens.php", [ "action" => "delete", "tokenid" => $data->id ] ); return \html_writer::link($tokenpageurl, get_string("delete")); } /** * Generate the validuntil column. * * @param \stdClass $data Data for the current row * @return string Content for the column */ public function col_validuntil($data) { if (empty($data->validuntil)) { return ''; } else { return userdate($data->validuntil, get_string('strftimedatetime', 'langconfig')); } } /** * Generate the fullname column. Also includes capabilities the user is missing for the webservice (if any) * * @param \stdClass $data Data for the current row * @return string Content for the column */ public function col_fullname($data) { global $OUTPUT; $identity = []; foreach ($this->userextrafields as $userextrafield) { $identity[] = s($data->$userextrafield); } $userprofilurl = new \moodle_url('/user/profile.php', ['id' => $data->userid]); $content = \html_writer::link($userprofilurl, fullname($data, $this->hasviewfullnames)); if ($identity) { $content .= \html_writer::div('<small>' . implode(', ', $identity) . '</small>', 'useridentity text-muted'); } // Make up list of capabilities that the user is missing for the given webservice. $webservicemanager = new \webservice(); $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users([['id' => $data->userid]], $data->serviceid); if ($data->serviceshortname <> MOODLE_OFFICIAL_MOBILE_SERVICE && !is_siteadmin($data->userid) && array_key_exists($data->userid, $usermissingcaps)) { $count = \html_writer::span(count($usermissingcaps[$data->userid]), 'badge badge-danger'); $links = array_map(function($capname) { return get_capability_docs_link((object)['name' => $capname]) . \html_writer::div($capname, 'text-muted'); }, $usermissingcaps[$data->userid]); $list = \html_writer::alist($links); $help = $OUTPUT->help_icon('missingcaps', 'webservice'); $content .= print_collapsible_region(\html_writer::div($list . $help, 'missingcaps'), 'small mt-2', \html_writer::random_id('usermissingcaps'), get_string('usermissingcaps', 'webservice', $count), '', true, true); } return $content; } /** * Generate the token column. * * @param \stdClass $data Data for the current row * @return string Content for the column */ public function col_token($data) { global $USER; // Hide the token if it wasn't created by the current user. if ($data->creatorid != $USER->id) { return \html_writer::tag('small', get_string('onlyseecreatedtokens', 'core_webservice'), ['class' => 'text-muted']); } return $data->token; } /** * Generate the creator column. * * @param \stdClass $data * @return string */ public function col_creatorlastname($data) { // We have loaded all the name fields for the creator, with the 'creator' prefix. // So just remove the prefix and make up a user object. $user = []; foreach ($data as $key => $value) { if (strpos($key, 'creator') !== false) { $newkey = str_replace('creator', '', $key); $user[$newkey] = $value; } } $creatorprofileurl = new \moodle_url('/user/profile.php', ['id' => $data->creatorid]); return \html_writer::link($creatorprofileurl, fullname((object)$user, $this->hasviewfullnames)); } /** * Format the service name column. * * @param \stdClass $data * @return string */ public function col_servicename($data) { return \html_writer::div(s($data->servicename)) . \html_writer::div(s($data->serviceshortname), 'small text-muted'); } /** * This function is used for the extra user fields. * * These are being dynamically added to the table so there are no functions 'col_<userfieldname>' as * the list has the potential to increase in the future and we don't want to have to remember to add * a new method to this class. We also don't want to pollute this class with unnecessary methods. * * @param string $colname The column name * @param \stdClass $data * @return string */ public function other_cols($colname, $data) { return s($data->{$colname}); } /** * Query the database for results to display in the table. * * Note: Initial bars are not implemented for this table because it includes user details twice and the initial bars do not work * when the user table is included more than once. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar Not implemented. Please pass false. */ public function query_db($pagesize, $useinitialsbar = false) { global $DB, $USER; if ($useinitialsbar) { debugging('Initial bar not implemented yet. Call out($pagesize, false)'); } $userfieldsapi = \core_user\fields::for_name(); $usernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $creatorfields = $userfieldsapi->get_sql('c', false, 'creator', '', false)->selects; if (!empty($this->userextrafields)) { $usernamefields .= ',u.' . implode(',u.', $this->userextrafields); } $params = ['tokenmode' => EXTERNAL_TOKEN_PERMANENT]; $selectfields = "SELECT t.id, t.token, t.iprestriction, t.validuntil, t.creatorid, u.id AS userid, $usernamefields, s.id AS serviceid, s.name AS servicename, s.shortname AS serviceshortname, $creatorfields "; $selectcount = "SELECT COUNT(t.id) "; $sql = " FROM {external_tokens} t JOIN {user} u ON u.id = t.userid JOIN {external_services} s ON s.id = t.externalserviceid JOIN {user} c ON c.id = t.creatorid WHERE t.tokentype = :tokenmode"; if (!$this->showalltokens) { // Only show tokens created by the current user. $sql .= " AND t.creatorid = :userid"; $params['userid'] = $USER->id; } if ($this->filterdata->token !== '') { $sql .= " AND " . $DB->sql_like("t.token", ":token"); $params['token'] = "%" . $DB->sql_like_escape($this->filterdata->token) . "%"; } if (!empty($this->filterdata->users)) { list($sqlusers, $paramsusers) = $DB->get_in_or_equal($this->filterdata->users, SQL_PARAMS_NAMED, 'user'); $sql .= " AND t.userid {$sqlusers}"; $params += $paramsusers; } if (!empty($this->filterdata->services)) { list($sqlservices, $paramsservices) = $DB->get_in_or_equal($this->filterdata->services, SQL_PARAMS_NAMED, 'service'); $sql .= " AND s.id {$sqlservices}"; $params += $paramsservices; } $sort = $this->get_sql_sort(); $sortsql = ''; if ($sort) { $sortsql = " ORDER BY {$sort}"; } $total = $DB->count_records_sql($selectcount . $sql, $params); $this->pagesize($pagesize, $total); $this->rawdata = $DB->get_recordset_sql($selectfields . $sql . $sortsql, $params, $this->get_page_start(), $this->get_page_size()); } } output/tagcollsearchable.php 0000644 00000005510 15151222237 0012255 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagcollsearchable * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; use lang_string; use core_tag_collection; /** * Class to display tag collection searchable control * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagcollsearchable extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass $tagcoll */ public function __construct($tagcoll) { $defaultid = core_tag_collection::get_default(); $editable = $tagcoll->id != $defaultid && has_capability('moodle/tag:manage', context_system::instance()); $edithint = new lang_string('editsearchable', 'core_tag'); $value = $tagcoll->searchable ? 1 : 0; parent::__construct('core_tag', 'tagcollsearchable', $tagcoll->id, $editable, $value, $value, $edithint); $this->set_type_toggle(); } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output * @return \stdClass */ public function export_for_template(\renderer_base $output) { if ($this->value) { $this->displayvalue = $output->pix_icon('i/checked', get_string('yes')); } else { $this->displayvalue = $output->pix_icon('i/unchecked', get_string('no')); } return parent::export_for_template($output); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { global $DB; require_capability('moodle/tag:manage', context_system::instance()); $tagcoll = $DB->get_record('tag_coll', array('id' => $itemid), '*', MUST_EXIST); core_tag_collection::update($tagcoll, array('searchable' => $newvalue)); return new self($tagcoll); } } output/tagname.php 0000644 00000004454 15151222237 0010240 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagname * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; use lang_string; use html_writer; use core_tag_tag; /** * Class to preapare a tag name for display. * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagname extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass|core_tag_tag $tag */ public function __construct($tag) { $editable = has_capability('moodle/tag:manage', context_system::instance()); $edithint = new lang_string('editname', 'core_tag'); $editlabel = new lang_string('newnamefor', 'core_tag', $tag->rawname); $value = $tag->rawname; $displayvalue = html_writer::link(core_tag_tag::make_url($tag->tagcollid, $tag->rawname), core_tag_tag::make_display_name($tag)); parent::__construct('core_tag', 'tagname', $tag->id, $editable, $displayvalue, $value, $edithint, $editlabel); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { require_capability('moodle/tag:manage', context_system::instance()); $tag = core_tag_tag::get($itemid, '*', MUST_EXIST); $tag->update(array('rawname' => $newvalue)); return new self($tag); } } output/tagcloud.php 0000644 00000007600 15151222237 0010422 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagindex * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use renderable; use templatable; use renderer_base; use stdClass; use moodle_url; use core_tag_tag; /** * Class to display a tag cloud - set of tags where each has a weight. * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagcloud implements templatable { /** @var array */ protected $tagset; /** @var int */ protected $totalcount; /** * Constructor * * @param array $tagset array of core_tag or stdClass elements, each of them must have attributes: * name, rawname, tagcollid * preferrably also have attributes: * isstandard, count, flag * @param int $totalcount total count of tags (for example to indicate that there are more tags than the count of tagset) * leave 0 if count of tagset is the actual count of tags * @param int $fromctx context id where this tag cloud is displayed * @param int $ctx context id for tag view link * @param int $rec recursive argument for tag view link */ public function __construct($tagset, $totalcount = 0, $fromctx = 0, $ctx = 0, $rec = 1) { $canmanagetags = has_capability('moodle/tag:manage', \context_system::instance()); $maxcount = 1; foreach ($tagset as $tag) { if (isset($tag->count) && $tag->count > $maxcount) { $maxcount = $tag->count; } } $this->tagset = array(); foreach ($tagset as $idx => $tag) { $this->tagset[$idx] = new stdClass(); $this->tagset[$idx]->name = core_tag_tag::make_display_name($tag, false); if ($canmanagetags && !empty($tag->flag)) { $this->tagset[$idx]->flag = 1; } $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $fromctx, $ctx, $rec); $this->tagset[$idx]->viewurl = $viewurl->out(false); if (isset($tag->isstandard)) { $this->tagset[$idx]->isstandard = $tag->isstandard ? 1 : 0; } if (!empty($tag->count)) { $this->tagset[$idx]->count = $tag->count; $this->tagset[$idx]->size = (int)($tag->count / $maxcount * 20); } } $this->totalcount = $totalcount ? $totalcount : count($this->tagset); } /** * Returns number of tags in the cloud * @return int */ public function get_count() { return count($this->tagset); } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { $cnt = count($this->tagset); return (object)array( 'tags' => $this->tagset, 'tagscount' => $cnt, 'totalcount' => $this->totalcount, 'overflow' => ($this->totalcount > $cnt) ? 1 : 0, ); } } output/tagindex.php 0000644 00000012440 15151222237 0010421 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagindex * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use renderable; use templatable; use renderer_base; use stdClass; use moodle_url; use core_tag_tag; /** * Class to display items tagged with a specific tag * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagindex implements templatable { /** @var core_tag_tag|stdClass */ protected $tag; /** @var stdClass */ protected $tagarea; /** @var stdClass */ protected $record; /** * Constructor * * @param core_tag_tag|stdClass $tag * @param string $component * @param string $itemtype * @param string $content * @param bool $exclusivemode * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where we need to search for items * @param int $rec search items in sub contexts as well * @param int $page * @param bool $totalpages */ public function __construct($tag, $component, $itemtype, $content, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0, $totalpages = 1) { $this->record = new stdClass(); $this->tag = $tag; $tagareas = \core_tag_area::get_areas(); if (!isset($tagareas[$itemtype][$component])) { throw new \coding_exception('Tag area for component '.$component.' and itemtype '.$itemtype.' is not defined'); } $this->tagarea = $tagareas[$itemtype][$component]; $this->record->tagid = $tag->id; $this->record->ta = $this->tagarea->id; $this->record->itemtype = $itemtype; $this->record->component = $component; $a = (object)array( 'tagarea' => \core_tag_area::display_name($component, $itemtype), 'tag' => \core_tag_tag::make_display_name($tag) ); if ($exclusivemode) { $this->record->title = get_string('itemstaggedwith', 'tag', $a); } else { $this->record->title = (string)$a->tagarea; } $this->record->content = $content; $this->record->nextpageurl = null; $this->record->prevpageurl = null; $this->record->exclusiveurl = null; $url = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, $exclusivemode, $fromctx, $ctx, $rec); $urlparams = array('ta' => $this->tagarea->id); if ($totalpages > $page + 1) { $this->record->nextpageurl = new moodle_url($url, $urlparams + array('page' => $page + 1)); } if ($page > 0) { $this->record->prevpageurl = new moodle_url($url, $urlparams + array('page' => $page - 1)); } if (!$exclusivemode && ($totalpages > 1 || $page)) { $this->record->exclusiveurl = new moodle_url($url, $urlparams + array('excl' => 1)); } $this->record->exclusivetext = get_string('exclusivemode', 'tag', $a); $this->record->hascontent = ($totalpages > 1 || $page || $content); $this->record->anchor = $component . '_' . $itemtype; } /** * Magic setter * * @param string $name * @param mixed $value */ public function __set($name, $value) { $this->record->$name = $value; } /** * Magic getter * * @param string $name * @return mixed */ public function __get($name) { return $this->record->$name; } /** * Magic isset * * @param string $name * @return bool */ public function __isset($name) { return isset($this->record->$name); } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { if ($this->record->nextpageurl && $this->record->nextpageurl instanceof moodle_url) { $this->record->nextpageurl = $this->record->nextpageurl->out(false); } if ($this->record->prevpageurl && $this->record->prevpageurl instanceof moodle_url) { $this->record->prevpageurl = $this->record->prevpageurl->out(false); } if ($this->record->exclusiveurl && $this->record->exclusiveurl instanceof moodle_url) { $this->record->exclusiveurl = $this->record->exclusiveurl->out(false); } return $this->record; } } output/tag.php 0000644 00000006117 15151222237 0007375 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tag * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use renderable; use templatable; use renderer_base; use stdClass; use moodle_url; use core_tag_tag; /** * Class to help display tag * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tag implements renderable, templatable { /** @var core_tag_tag|stdClass */ protected $record; /** * Constructor * * @param core_tag_tag|stdClass $tag */ public function __construct($tag) { if ($tag instanceof core_tag_tag) { $this->record = $tag; return; } $tag = (array)$tag + array( 'name' => '', 'rawname' => '', 'description' => '', 'descriptionformat' => FORMAT_HTML, 'flag' => 0, 'isstandard' => 0, 'id' => 0, 'tagcollid' => 0, ); $this->record = (object)$tag; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $CFG; require_once($CFG->libdir . '/externallib.php'); $r = new stdClass(); $r->id = (int)$this->record->id; $r->tagcollid = clean_param($this->record->tagcollid, PARAM_INT); $r->rawname = clean_param($this->record->rawname, PARAM_TAG); $r->name = clean_param($this->record->name, PARAM_TAG); $format = clean_param($this->record->descriptionformat, PARAM_INT); list($r->description, $r->descriptionformat) = external_format_text($this->record->description, $format, \context_system::instance()->id, 'core', 'tag', $r->id); $r->flag = clean_param($this->record->flag, PARAM_INT); if (isset($this->record->isstandard)) { $r->isstandard = clean_param($this->record->isstandard, PARAM_INT) ? 1 : 0; } $r->official = $r->isstandard; // For backwards compatibility. $url = core_tag_tag::make_url($r->tagcollid, $r->rawname); $r->viewurl = $url->out(false); return $r; } } output/tagisstandard.php 0000644 00000005352 15151222237 0011452 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagisstandard * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; use core_tag_tag; /** * Class to display/toggle tag isstandard attribute * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagisstandard extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass|core_tag_tag $tag */ public function __construct($tag) { $editable = has_capability('moodle/tag:manage', context_system::instance()); $value = (int)(bool)$tag->isstandard; parent::__construct('core_tag', 'tagisstandard', $tag->id, $editable, $value, $value); $this->set_type_toggle(); } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output * @return \stdClass */ public function export_for_template(\renderer_base $output) { if ($this->value) { $this->edithint = get_string('settypedefault', 'core_tag'); $this->displayvalue = $output->pix_icon('i/checked', $this->edithint); } else { $this->edithint = get_string('settypestandard', 'core_tag'); $this->displayvalue = $output->pix_icon('i/unchecked', $this->edithint); } return parent::export_for_template($output); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { require_capability('moodle/tag:manage', context_system::instance()); $tag = core_tag_tag::get($itemid, '*', MUST_EXIST); $newvalue = (int)clean_param($newvalue, PARAM_BOOL); $tag->update(array('isstandard' => $newvalue)); return new self($tag); } } output/tagcollname.php 0000644 00000004726 15151222237 0011114 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagcollname * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; use lang_string; use html_writer; use core_tag_collection; use moodle_url; /** * Class to preapare a tag name for display. * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagcollname extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass $tagcoll */ public function __construct($tagcoll) { $editable = has_capability('moodle/tag:manage', context_system::instance()); $edithint = new lang_string('editcollname', 'core_tag'); $value = $tagcoll->name; $name = \core_tag_collection::display_name($tagcoll); $editlabel = new lang_string('newcollnamefor', 'core_tag', $name); $manageurl = new moodle_url('/tag/manage.php', array('tc' => $tagcoll->id)); $displayvalue = html_writer::link($manageurl, $name); parent::__construct('core_tag', 'tagcollname', $tagcoll->id, $editable, $displayvalue, $value, $edithint, $editlabel); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { global $DB; require_capability('moodle/tag:manage', context_system::instance()); $tagcoll = $DB->get_record('tag_coll', array('id' => $itemid), '*', MUST_EXIST); \core_tag_collection::update($tagcoll, array('name' => $newvalue)); return new self($tagcoll); } } output/tagareashowstandard.php 0000644 00000005551 15151222237 0012651 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagareashowstandard * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; use lang_string; use core_tag_tag; use core_tag_area; /** * Class to display tag area show standard control * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagareashowstandard extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass $tagarea */ public function __construct($tagarea) { $editable = has_capability('moodle/tag:manage', context_system::instance()); $edithint = new lang_string('editisstandard', 'core_tag'); $value = $tagarea->showstandard; $areaname = core_tag_area::display_name($tagarea->component, $tagarea->itemtype); $editlabel = new lang_string('changeshowstandard', 'core_tag', $areaname); parent::__construct('core_tag', 'tagareashowstandard', $tagarea->id, $editable, null, $value, $edithint, $editlabel); $standardchoices = array( core_tag_tag::BOTH_STANDARD_AND_NOT => get_string('standardsuggest', 'tag'), core_tag_tag::STANDARD_ONLY => get_string('standardforce', 'tag'), core_tag_tag::HIDE_STANDARD => get_string('standardhide', 'tag') ); $this->set_type_select($standardchoices); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { global $DB; require_capability('moodle/tag:manage', context_system::instance()); $tagarea = $DB->get_record('tag_area', array('id' => $itemid), '*', MUST_EXIST); $newvalue = clean_param($newvalue, PARAM_INT); $data = array('showstandard' => $newvalue); core_tag_area::update($tagarea, $data); $tagarea->showstandard = $newvalue; $tmpl = new self($tagarea); return $tmpl; } } output/tagareacollection.php 0000644 00000006525 15151222237 0012305 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagareacollection * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; use lang_string; use core_tag_area; /** * Class to display collection select for the tag area * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagareacollection extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass $tagarea */ public function __construct($tagarea) { if (!empty($tagarea->locked)) { // If the tag collection for the current tag area is locked, display the // name of the collection without possibility to edit it. $tagcoll = \core_tag_collection::get_by_id($tagarea->tagcollid); parent::__construct('core_tag', 'tagareacollection', $tagarea->id, false, \core_tag_collection::display_name($tagcoll), $tagarea->tagcollid); return; } $tagcollections = \core_tag_collection::get_collections_menu(true); $editable = (count($tagcollections) > 1) && has_capability('moodle/tag:manage', context_system::instance()); $areaname = core_tag_area::display_name($tagarea->component, $tagarea->itemtype); $edithint = new lang_string('edittagcollection', 'core_tag'); $editlabel = new lang_string('changetagcoll', 'core_tag', $areaname); $value = $tagarea->tagcollid; parent::__construct('core_tag', 'tagareacollection', $tagarea->id, $editable, null, $value, $edithint, $editlabel); $this->set_type_select($tagcollections); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { global $DB; require_capability('moodle/tag:manage', \context_system::instance()); $tagarea = $DB->get_record('tag_area', array('id' => $itemid), '*', MUST_EXIST); $newvalue = clean_param($newvalue, PARAM_INT); $tagcollections = \core_tag_collection::get_collections_menu(true); if (!array_key_exists($newvalue, $tagcollections)) { throw new \moodle_exception('invalidparameter', 'debug'); } $data = array('tagcollid' => $newvalue); core_tag_area::update($tagarea, $data); $tagarea->tagcollid = $newvalue; $tmpl = new self($tagarea); return $tmpl; } } output/tagfeed.php 0000644 00000004772 15151222237 0010226 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagfeed * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use templatable; use renderer_base; use stdClass; /** * Class to display feed of tagged items * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagfeed implements templatable { /** @var array */ protected $items; /** * Constructor * * Usually the most convenient way is to call constructor without arguments and * add items later using add() method. * * @param array $items */ public function __construct($items = array()) { $this->items = array(); if ($items) { foreach ($items as $item) { $item = (array)$item + array('img' => '', 'heading' => '', 'details' => ''); $this->add($item['img'], $item['heading'], $item['details']); } } } /** * Adds one item to the tagfeed * * @param string $img HTML code representing image (or image wrapped in a link), note that * core_tag/tagfeed template expects image to be 35x35 px * @param string $heading HTML for item heading * @param string $details HTML for item details (keep short) */ public function add($img, $heading, $details = '') { $this->items[] = array('img' => $img, 'heading' => $heading, 'details' => $details); } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { return array('items' => $this->items); } } output/tagareaenabled.php 0000644 00000005454 15151222237 0011544 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagareaenabled * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; /** * Class to display tag area enabled control * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagareaenabled extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass $tagarea */ public function __construct($tagarea) { $editable = has_capability('moodle/tag:manage', context_system::instance()); $value = $tagarea->enabled ? 1 : 0; parent::__construct('core_tag', 'tagareaenable', $tagarea->id, $editable, '', $value); $this->set_type_toggle(); } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output * @return \stdClass */ public function export_for_template(\renderer_base $output) { if ($this->value) { $this->edithint = get_string('disable'); $this->displayvalue = $output->pix_icon('i/hide', get_string('disable')); } else { $this->edithint = get_string('enable'); $this->displayvalue = $output->pix_icon('i/show', get_string('enable')); } return parent::export_for_template($output); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { global $DB; require_capability('moodle/tag:manage', context_system::instance()); $tagarea = $DB->get_record('tag_area', array('id' => $itemid), '*', MUST_EXIST); $newvalue = $newvalue ? 1 : 0; $data = array('enabled' => $newvalue); \core_tag_area::update($tagarea, $data); $tagarea->enabled = $newvalue; $tmpl = new self($tagarea); return $tmpl; } } output/taglist.php 0000644 00000010362 15151222237 0010266 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\taglist * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use templatable; use renderer_base; use stdClass; use core_tag_tag; use context; /** * Class to preapare a list of tags for display, usually the list of tags some entry is tagged with. * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class taglist implements templatable { /** @var array */ protected $tags; /** @var string */ protected $label; /** @var bool $accesshidelabel if true, the label should have class="accesshide" added. */ protected $accesshidelabel; /** @var string */ protected $classes; /** @var int */ protected $limit; /** * Constructor * * @param array $tags list of instances of \core_tag_tag or \stdClass * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null * to use default, set to '' (empty string) to omit the label completely * @param string $classes additional classes for the enclosing div element * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link * will be appended to the end, JS will toggle the rest of the tags. 0 means no limit. * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link * @param bool $accesshidelabel if true, the label should have class="accesshide" added. */ public function __construct($tags, $label = null, $classes = '', $limit = 10, $pagecontext = null, $accesshidelabel = false) { global $PAGE; $canmanagetags = has_capability('moodle/tag:manage', \context_system::instance()); $this->label = ($label === null) ? get_string('tags') : $label; $this->accesshidelabel = $accesshidelabel; $this->classes = $classes; $fromctx = $pagecontext ? $pagecontext->id : (($PAGE->context->contextlevel == CONTEXT_SYSTEM) ? 0 : $PAGE->context->id); $this->tags = array(); foreach ($tags as $idx => $tag) { $this->tags[$idx] = new stdClass(); $this->tags[$idx]->name = core_tag_tag::make_display_name($tag, false); if ($canmanagetags && !empty($tag->flag)) { $this->tags[$idx]->flag = 1; } $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $fromctx); $this->tags[$idx]->viewurl = $viewurl->out(false); if (isset($tag->isstandard)) { $this->tags[$idx]->isstandard = $tag->isstandard ? 1 : 0; } if ($limit && count($this->tags) > $limit) { $this->tags[$idx]->overlimit = 1; } } $this->limit = $limit; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { $cnt = count($this->tags); return (object)array( 'tags' => array_values($this->tags), 'label' => $this->label, 'accesshidelabel' => $this->accesshidelabel, 'tagscount' => $cnt, 'overflow' => ($this->limit && $cnt > $this->limit) ? 1 : 0, 'classes' => $this->classes, ); } } output/tagflag.php 0000644 00000005464 15151222237 0010233 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag\output\tagflag * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\output; use context_system; use core_tag_tag; /** * Class to display tag flag toggle * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tagflag extends \core\output\inplace_editable { /** * Constructor. * * @param \stdClass|core_tag_tag $tag */ public function __construct($tag) { $editable = has_capability('moodle/tag:manage', context_system::instance()); $value = (int)$tag->flag; parent::__construct('core_tag', 'tagflag', $tag->id, $editable, $value, $value); $this->set_type_toggle(array(0, $value ? $value : 1)); } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output * @return \stdClass */ public function export_for_template(\renderer_base $output) { if ($this->value) { $this->edithint = get_string('resetflag', 'core_tag'); $this->displayvalue = $output->pix_icon('i/flagged', $this->edithint) . " ({$this->value})"; } else { $this->edithint = get_string('flagasinappropriate', 'core_tag'); $this->displayvalue = $output->pix_icon('i/unflagged', $this->edithint); } return parent::export_for_template($output); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { require_capability('moodle/tag:manage', context_system::instance()); $tag = core_tag_tag::get($itemid, '*', MUST_EXIST); $newvalue = (int)clean_param($newvalue, PARAM_BOOL); if ($newvalue) { $tag->flag(); } else { $tag->reset_flag(); } return new self($tag); } } areas_table.php 0000644 00000007051 15151222237 0007522 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag_areas_table * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Table with the list of available tag areas for "Manage tags" page. * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_areas_table extends html_table { /** * Constructor * * @param string|moodle_url $pageurl */ public function __construct($pageurl) { global $OUTPUT; parent::__construct(); $this->attributes['class'] = 'generaltable tag-areas-table'; $this->head = array( get_string('tagareaname', 'core_tag'), get_string('component', 'tag'), get_string('tagareaenabled', 'core_tag'), get_string('tagcollection', 'tag'), get_string('showstandard', 'tag') . $OUTPUT->help_icon('showstandard', 'tag') ); $this->data = array(); $this->rowclasses = array(); $tagareas = core_tag_area::get_areas(); $tagcollections = core_tag_collection::get_collections_menu(true); $tagcollectionsall = core_tag_collection::get_collections_menu(); $standardchoices = array( core_tag_tag::BOTH_STANDARD_AND_NOT => get_string('standardsuggest', 'tag'), core_tag_tag::STANDARD_ONLY => get_string('standardforce', 'tag'), core_tag_tag::HIDE_STANDARD => get_string('standardhide', 'tag') ); foreach ($tagareas as $itemtype => $it) { foreach ($it as $component => $record) { $areaname = core_tag_area::display_name($record->component, $record->itemtype); $tmpl = new \core_tag\output\tagareaenabled($record); $enabled = $OUTPUT->render_from_template('core/inplace_editable', $tmpl->export_for_template($OUTPUT)); $tmpl = new \core_tag\output\tagareacollection($record); $collectionselect = $OUTPUT->render_from_template('core/inplace_editable', $tmpl->export_for_template($OUTPUT)); $tmpl = new \core_tag\output\tagareashowstandard($record); $showstandardselect = $OUTPUT->render_from_template('core/inplace_editable', $tmpl->export_for_template($OUTPUT)); $this->data[] = array( $areaname, ($record->component === 'core' || preg_match('/^core_/', $record->component)) ? get_string('coresystem') : get_string('pluginname', $record->component), $enabled, $collectionselect, $showstandardselect ); $this->rowclasses[] = $record->enabled ? '' : 'dimmed_text'; } } } } renderer.php 0000644 00000015754 15151222237 0007077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag_renderer * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Class core_tag_renderer * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_renderer extends plugin_renderer_base { /** * Renders the tag search page * * @param string $query * @param int $tagcollid * @return string */ public function tag_search_page($query = '', $tagcollid = 0) { $rv = $this->output->heading(get_string('searchtags', 'tag'), 2); $searchbox = $this->search_form($query, $tagcollid); $rv .= html_writer::div($searchbox, '', array('id' => 'tag-search-box')); $tagcloud = core_tag_collection::get_tag_cloud($tagcollid, false, 150, 'name', $query); $searchresults = ''; if ($tagcloud->get_count()) { $searchresults = $this->output->render_from_template('core_tag/tagcloud', $tagcloud->export_for_template($this->output)); $rv .= html_writer::div($searchresults, '', array('id' => 'tag-search-results')); } else if (strval($query) !== '') { $rv .= '<div class="tag-search-empty">' . get_string('notagsfound', 'tag', s($query)) . '</div>'; } return $rv; } /** * Renders the tag index page * * @param core_tag_tag $tag * @param \core_tag\output\tagindex[] $entities * @param int $tagareaid * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return string */ public function tag_index_page($tag, $entities, $tagareaid, $exclusivemode, $fromctx, $ctx, $rec, $page) { global $CFG; $this->page->requires->js_call_amd('core/tag', 'initTagindexPage'); $tagname = $tag->get_display_name(); $systemcontext = context_system::instance(); if ($tag->flag > 0 && has_capability('moodle/tag:manage', $systemcontext)) { $tagname = '<span class="flagged-tag">' . $tagname . '</span>'; } $rv = ''; $rv .= $this->output->heading($tagname, 2); $rv .= $this->tag_links($tag); if ($desciption = $tag->get_formatted_description()) { $rv .= $this->output->box($desciption, 'generalbox tag-description'); } $relatedtagslimit = 10; $relatedtags = $tag->get_related_tags(); $taglist = new \core_tag\output\taglist($relatedtags, get_string('relatedtags', 'tag'), 'tag-relatedtags', $relatedtagslimit); $rv .= $this->output->render_from_template('core_tag/taglist', $taglist->export_for_template($this->output)); // Display quick menu of the item types (if more than one item type found). $entitylinks = array(); foreach ($entities as $entity) { if (!empty($entity->hascontent)) { $entitylinks[] = '<li><a href="#'.$entity->anchor.'">' . core_tag_area::display_name($entity->component, $entity->itemtype) . '</a></li>'; } } if (count($entitylinks) > 1) { $rv .= '<div class="tag-index-toc"><ul class="inline-list">' . join('', $entitylinks) . '</ul></div>'; } else if (!$entitylinks) { $rv .= '<div class="tag-noresults">' . get_string('noresultsfor', 'tag', $tagname) . '</div>'; } // Display entities tagged with the tag. $content = ''; foreach ($entities as $entity) { if (!empty($entity->hascontent)) { $content .= $this->output->render_from_template('core_tag/index', $entity->export_for_template($this->output)); } } if ($exclusivemode) { $rv .= $content; } else if ($content) { $rv .= html_writer::div($content, 'tag-index-items'); } // Display back link if we are browsing one tag area. if ($tagareaid) { $url = $tag->get_view_url(0, $fromctx, $ctx, $rec); $rv .= '<div class="tag-backtoallitems">' . html_writer::link($url, get_string('backtoallitems', 'tag', $tag->get_display_name())) . '</div>'; } return $rv; } /** * Prints a box that contains the management links of a tag * * @param core_tag_tag $tag * @return string */ protected function tag_links($tag) { if ($links = $tag->get_links()) { $content = '<ul class="inline-list"><li>' . implode('</li> <li>', $links) . '</li></ul>'; return html_writer::div($content, 'tag-management-box'); } return ''; } /** * Prints the tag search box * * @param string $query last search string * @param int $tagcollid last selected tag collection id * @return string */ protected function search_form($query = '', $tagcollid = 0) { $searchurl = new moodle_url('/tag/search.php'); $output = '<form action="' . $searchurl . '">'; $output .= '<label class="accesshide" for="searchform_query">' . get_string('searchtags', 'tag') . '</label>'; $output .= '<input id="searchform_query" name="query" type="text" size="40" value="' . s($query) . '" />'; $tagcolls = core_tag_collection::get_collections_menu(false, true, get_string('inalltagcoll', 'tag')); if (count($tagcolls) > 1) { $output .= '<label class="accesshide" for="searchform_tc">' . get_string('selectcoll', 'tag') . '</label>'; $output .= html_writer::select($tagcolls, 'tc', $tagcollid, null, array('id' => 'searchform_tc')); } $output .= '<input name="go" type="submit" size="40" value="' . s(get_string('search', 'tag')) . '" />'; $output .= '</form>'; return $output; } } collection.php 0000644 00000035671 15151222237 0007424 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class to manage tag collections * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Class to manage tag collections * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_collection { /** @var string used for function cloud_sort() */ public static $cloudsortfield = 'name'; /** * Returns the list of tag collections defined in the system. * * @param bool $onlysearchable only return collections that can be searched. * @return array array of objects where each object has properties: id, name, isdefault, itemtypes, sortorder */ public static function get_collections($onlysearchable = false) { global $DB; $cache = cache::make('core', 'tags'); if (($tagcolls = $cache->get('tag_coll')) === false) { // Retrieve records from DB and create a default one if it is not present. $tagcolls = $DB->get_records('tag_coll', null, 'isdefault DESC, sortorder, id'); if (empty($tagcolls)) { // When this method is called for the first time it automatically creates the default tag collection. $DB->insert_record('tag_coll', array('isdefault' => 1, 'sortorder' => 0)); $tagcolls = $DB->get_records('tag_coll'); } else { // Make sure sortorder is correct. $idx = 0; foreach ($tagcolls as $id => $tagcoll) { if ($tagcoll->sortorder != $idx) { $DB->update_record('tag_coll', array('sortorder' => $idx, 'id' => $id)); $tagcolls[$id]->sortorder = $idx; } $idx++; } } $cache->set('tag_coll', $tagcolls); } if ($onlysearchable) { $rv = array(); foreach ($tagcolls as $id => $tagcoll) { if ($tagcoll->searchable) { $rv[$id] = $tagcoll; } } return $rv; } return $tagcolls; } /** * Returns the tag collection object * * @param int $tagcollid * @return stdClass */ public static function get_by_id($tagcollid) { $tagcolls = self::get_collections(); if (array_key_exists($tagcollid, $tagcolls)) { return $tagcolls[$tagcollid]; } return null; } /** * Returns the list of existing tag collections as id=>name * * @param bool $unlockedonly * @param bool $onlysearchable * @param string $selectalllabel * @return array */ public static function get_collections_menu($unlockedonly = false, $onlysearchable = false, $selectalllabel = null) { $tagcolls = self::get_collections($onlysearchable); $options = array(); foreach ($tagcolls as $id => $tagcoll) { if (!$unlockedonly || empty($tagcoll->component)) { $options[$id] = self::display_name($tagcoll); } } if (count($options) > 1 && $selectalllabel) { $options = array(0 => $selectalllabel) + $options; } return $options; } /** * Returns id of the default tag collection * * @return int */ public static function get_default() { $collections = self::get_collections(); $keys = array_keys($collections); return $keys[0]; } /** * Returns formatted name of the tag collection * * @param stdClass $record record from DB table tag_coll * @return string */ public static function display_name($record) { $syscontext = context_system::instance(); if (!empty($record->component)) { $identifier = 'tagcollection_' . clean_param($record->name, PARAM_STRINGID); $component = $record->component; if ($component === 'core') { $component = 'tag'; } return get_string($identifier, $component); } if (!empty($record->name)) { return format_string($record->name, true, array('context' => $syscontext)); } else if ($record->isdefault) { return get_string('defautltagcoll', 'tag'); } else { return $record->id; } } /** * Returns all tag areas in the given tag collection * * @param int $tagcollid * @return array */ public static function get_areas($tagcollid) { $allitemtypes = core_tag_area::get_areas($tagcollid, true); $itemtypes = array(); foreach ($allitemtypes as $itemtype => $it) { foreach ($it as $component => $v) { $itemtypes[$v->id] = $v; } } return $itemtypes; } /** * Returns the list of names of areas (enabled only) that are in this collection. * * @param int $tagcollid * @return array */ public static function get_areas_names($tagcollid, $enabledonly = true) { $allitemtypes = core_tag_area::get_areas($tagcollid, $enabledonly); $itemtypes = array(); foreach ($allitemtypes as $itemtype => $it) { foreach ($it as $component => $v) { $itemtypes[$v->id] = core_tag_area::display_name($component, $itemtype); } } return $itemtypes; } /** * Creates a new tag collection * * @param stdClass $data data from form core_tag_collection_form * @return int|false id of created tag collection or false if failed */ public static function create($data) { global $DB; $data = (object)$data; $tagcolls = self::get_collections(); $tagcoll = (object)array( 'name' => $data->name, 'isdefault' => 0, 'component' => !empty($data->component) ? $data->component : null, 'sortorder' => count($tagcolls), 'searchable' => isset($data->searchable) ? (int)(bool)$data->searchable : 1, 'customurl' => !empty($data->customurl) ? $data->customurl : null, ); $tagcoll->id = $DB->insert_record('tag_coll', $tagcoll); // Reset cache. cache::make('core', 'tags')->delete('tag_coll'); \core\event\tag_collection_created::create_from_record($tagcoll)->trigger(); return $tagcoll; } /** * Updates the tag collection information * * @param stdClass $tagcoll existing record in DB table tag_coll * @param stdClass $data data to update * @return bool wether the record was updated */ public static function update($tagcoll, $data) { global $DB; $defaulttagcollid = self::get_default(); $allowedfields = array('name', 'searchable', 'customurl'); if ($tagcoll->id == $defaulttagcollid) { $allowedfields = array('name'); } $updatedata = array(); $data = (array)$data; foreach ($allowedfields as $key) { if (array_key_exists($key, $data) && $data[$key] !== $tagcoll->$key) { $updatedata[$key] = $data[$key]; } } if (!$updatedata) { // Nothing to update. return false; } if (isset($updatedata['searchable'])) { $updatedata['searchable'] = (int)(bool)$updatedata['searchable']; } foreach ($updatedata as $key => $value) { $tagcoll->$key = $value; } $updatedata['id'] = $tagcoll->id; $DB->update_record('tag_coll', $updatedata); // Reset cache. cache::make('core', 'tags')->delete('tag_coll'); \core\event\tag_collection_updated::create_from_record($tagcoll)->trigger(); return true; } /** * Deletes a custom tag collection * * @param stdClass $tagcoll existing record in DB table tag_coll * @return bool wether the tag collection was deleted */ public static function delete($tagcoll) { global $DB, $CFG; $defaulttagcollid = self::get_default(); if ($tagcoll->id == $defaulttagcollid) { return false; } // Move all tags from this tag collection to the default one. $allitemtypes = core_tag_area::get_areas($tagcoll->id); foreach ($allitemtypes as $it) { foreach ($it as $v) { core_tag_area::update($v, array('tagcollid' => $defaulttagcollid)); } } // Delete tags from this tag_coll. core_tag_tag::delete_tags($DB->get_fieldset_select('tag', 'id', 'tagcollid = ?', array($tagcoll->id))); // Delete the tag collection. $DB->delete_records('tag_coll', array('id' => $tagcoll->id)); // Reset cache. cache::make('core', 'tags')->delete('tag_coll'); \core\event\tag_collection_deleted::create_from_record($tagcoll)->trigger(); return true; } /** * Moves the tag collection in the list one position up or down * * @param stdClass $tagcoll existing record in DB table tag_coll * @param int $direction move direction: +1 or -1 * @return bool */ public static function change_sortorder($tagcoll, $direction) { global $DB; if ($direction != -1 && $direction != 1) { throw new coding_exception('Second argument in tag_coll_change_sortorder() can be only 1 or -1'); } $tagcolls = self::get_collections(); $keys = array_keys($tagcolls); $idx = array_search($tagcoll->id, $keys); if ($idx === false || $idx == 0 || $idx + $direction < 1 || $idx + $direction >= count($tagcolls)) { return false; } $otherid = $keys[$idx + $direction]; $DB->update_record('tag_coll', array('id' => $tagcoll->id, 'sortorder' => $idx + $direction)); $DB->update_record('tag_coll', array('id' => $otherid, 'sortorder' => $idx)); // Reset cache. cache::make('core', 'tags')->delete('tag_coll'); return true; } /** * Permanently deletes all non-standard tags that no longer have any instances pointing to them * * @param array $collections optional list of tag collections ids to cleanup */ public static function cleanup_unused_tags($collections = null) { global $DB, $CFG; $params = array(); $sql = "SELECT tg.id FROM {tag} tg LEFT OUTER JOIN {tag_instance} ti ON ti.tagid = tg.id WHERE ti.id IS NULL AND tg.isstandard = 0"; if ($collections) { list($sqlcoll, $params) = $DB->get_in_or_equal($collections, SQL_PARAMS_NAMED); $sql .= " AND tg.tagcollid " . $sqlcoll; } if ($unusedtags = $DB->get_fieldset_sql($sql, $params)) { core_tag_tag::delete_tags($unusedtags); } } /** * Returns the list of tags with number of items tagged * * @param int $tagcollid * @param null|bool $isstandard return only standard tags * @param int $limit maximum number of tags to retrieve, tags are sorted by the instance count * descending here regardless of $sort parameter * @param string $sort sort order for display, default 'name' - tags will be sorted after they are retrieved * @param string $search search string * @param int $fromctx context id where this tag cloud is displayed * @param int $ctx only retrieve tag instances in this context * @param int $rec retrieve tag instances in the $ctx context and it's children (default 1) * @return \core_tag\output\tagcloud */ public static function get_tag_cloud($tagcollid, $isstandard = false, $limit = 150, $sort = 'name', $search = '', $fromctx = 0, $ctx = 0, $rec = 1) { global $DB; $fromclause = 'FROM {tag_instance} ti JOIN {tag} tg ON tg.id = ti.tagid'; $whereclause = 'WHERE ti.itemtype <> \'tag\''; list($sql, $params) = $DB->get_in_or_equal($tagcollid ? array($tagcollid) : array_keys(self::get_collections(true))); $whereclause .= ' AND tg.tagcollid ' . $sql; if ($isstandard) { $whereclause .= ' AND tg.isstandard = 1'; } $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); if ($rec && $context->contextlevel != CONTEXT_SYSTEM) { $fromclause .= ' JOIN {context} ctx ON ctx.id = ti.contextid '; $whereclause .= ' AND ctx.path LIKE ?'; $params[] = $context->path . '%'; } else if (!$rec) { $whereclause .= ' AND ti.contextid = ?'; $params[] = $context->id; } if (strval($search) !== '') { $whereclause .= ' AND tg.name LIKE ?'; $params[] = '%' . core_text::strtolower($search) . '%'; } $tagsincloud = $DB->get_records_sql( "SELECT tg.id, tg.rawname, tg.name, tg.isstandard, COUNT(ti.id) AS count, tg.flag, tg.tagcollid $fromclause $whereclause GROUP BY tg.id, tg.rawname, tg.name, tg.flag, tg.isstandard, tg.tagcollid ORDER BY count DESC, tg.name ASC", $params, 0, $limit); $tagscount = count($tagsincloud); if ($tagscount == $limit) { $tagscount = $DB->get_field_sql("SELECT COUNT(DISTINCT tg.id) $fromclause $whereclause", $params); } self::$cloudsortfield = $sort; usort($tagsincloud, "self::cloud_sort"); return new core_tag\output\tagcloud($tagsincloud, $tagscount, $fromctx, $ctx, $rec); } /** * This function is used to sort the tags in the cloud. * * @param string $a Tag name to compare against $b * @param string $b Tag name to compare against $a * @return int The result of the comparison/validation 1, 0 or -1 */ public static function cloud_sort($a, $b) { $tagsort = self::$cloudsortfield ?: 'name'; if (is_numeric($a->$tagsort)) { return (($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort)) ? 1 : -1; } else if (is_string($a->$tagsort)) { return strcmp($a->$tagsort, $b->$tagsort); } else { return 0; } } } area.php 0000644 00000052236 15151222237 0006175 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class core_tag_area for managing tag areas * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Class to manage tag areas * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_area { /** * Returns the list of areas indexed by itemtype and component * * @param int $tagcollid return only areas in this tag collection * @param bool $enabledonly return only enabled tag areas * @return array itemtype=>component=>tagarea object */ public static function get_areas($tagcollid = null, $enabledonly = false) { global $DB; $cache = cache::make('core', 'tags'); if (($itemtypes = $cache->get('tag_area')) === false) { $colls = core_tag_collection::get_collections(); $defaultcoll = reset($colls); $itemtypes = array(); $areas = $DB->get_records('tag_area', array(), 'component,itemtype'); foreach ($areas as $area) { if ($colls[$area->tagcollid]->component) { $area->locked = true; } $itemtypes[$area->itemtype][$area->component] = $area; } $cache->set('tag_area', $itemtypes); } if ($tagcollid || $enabledonly) { $rv = array(); foreach ($itemtypes as $itemtype => $it) { foreach ($it as $component => $v) { if (($v->tagcollid == $tagcollid || !$tagcollid) && (!$enabledonly || $v->enabled)) { $rv[$itemtype][$component] = $v; } } } return $rv; } return $itemtypes; } /** * Retrieves info about one tag area * * @param int $tagareaid * @return stdClass */ public static function get_by_id($tagareaid) { $tagareas = self::get_areas(); foreach ($tagareas as $itemtype => $it) { foreach ($it as $component => $v) { if ($v->id == $tagareaid) { return $v; } } } return null; } /** * Returns the display name for this area * * @param string $component * @param string $itemtype * @return lang_string */ public static function display_name($component, $itemtype) { $identifier = 'tagarea_' . clean_param($itemtype, PARAM_STRINGID); if ($component === 'core') { $component = 'tag'; } return new lang_string($identifier, $component); } /** * Returns whether the tag area is enabled * * @param string $component component responsible for tagging * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. * @return bool|null */ public static function is_enabled($component, $itemtype) { global $CFG; if (empty($CFG->usetags)) { return false; } $itemtypes = self::get_areas(); if (isset($itemtypes[$itemtype][$component])) { return $itemtypes[$itemtype][$component]->enabled ? true : false; } return null; } /** * Checks if the tag area allows items to be tagged in multiple different contexts. * * If true then it indicates that not all tag instance contexts must match the * context of the item they are tagging. If false then all tag instance should * match the context of the item they are tagging. * * Example use case for multi-context tagging: * A question that exists in a course category context may be used by multiple * child courses. The question tag area can allow tag instances to be created in * multiple contexts which allows the tag API to tag the question at the course * category context and then seperately in each of the child course contexts. * * @param string $component component responsible for tagging * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. * @return bool */ public static function allows_tagging_in_multiple_contexts($component, $itemtype) { $itemtypes = self::get_areas(); if (isset($itemtypes[$itemtype][$component])) { $config = $itemtypes[$itemtype][$component]; return isset($config->multiplecontexts) ? $config->multiplecontexts : false; } return false; } /** * Returns the id of the tag collection that should be used for storing tags of this itemtype * * @param string $component component responsible for tagging * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. * @return int */ public static function get_collection($component, $itemtype) { $itemtypes = self::get_areas(); if (array_key_exists($itemtype, $itemtypes)) { if (!array_key_exists($component, $itemtypes[$itemtype])) { $component = key($itemtypes[$itemtype]); } return $itemtypes[$itemtype][$component]->tagcollid; } return core_tag_collection::get_default(); } /** * Returns wether this tag area should display or not standard tags when user edits it. * * @param string $component component responsible for tagging * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. * @return int */ public static function get_showstandard($component, $itemtype) { $itemtypes = self::get_areas(); if (array_key_exists($itemtype, $itemtypes)) { if (!array_key_exists($component, $itemtypes[$itemtype])) { $component = key($itemtypes[$itemtype]); } return $itemtypes[$itemtype][$component]->showstandard; } return core_tag_tag::BOTH_STANDARD_AND_NOT; } /** * Returns all tag areas and collections that are currently cached in DB for this component * * @param string $componentname * @return array first element is the list of areas and the second list of collections */ protected static function get_definitions_for_component($componentname) { global $DB; list($a, $b) = core_component::normalize_component($componentname); $component = $b ? ($a . '_' . $b) : $a; $sql = 'component = :component'; $params = array('component' => $component); if ($component === 'core') { $sql .= ' OR component LIKE :coreprefix'; $params['coreprefix'] = 'core_%'; } $fields = $DB->sql_concat_join("':'", array('itemtype', 'component')); $existingareas = $DB->get_records_sql( "SELECT $fields AS returnkey, a.* FROM {tag_area} a WHERE $sql", $params); $fields = $DB->sql_concat_join("':'", array('name', 'component')); $existingcolls = $DB->get_records_sql( "SELECT $fields AS returnkey, t.* FROM {tag_coll} t WHERE $sql", $params); return array($existingareas, $existingcolls); } /** * Completely delete a tag area and all instances inside it * * @param stdClass $record */ protected static function delete($record) { global $DB; core_tag_tag::delete_instances($record->component, $record->itemtype); $DB->delete_records('tag_area', array('itemtype' => $record->itemtype, 'component' => $record->component)); // Reset cache. cache::make('core', 'tags')->delete('tag_area'); } /** * Create a new tag area * * @param stdClass $record */ protected static function create($record) { global $DB; if (empty($record->tagcollid)) { $record->tagcollid = core_tag_collection::get_default(); } $DB->insert_record('tag_area', array('component' => $record->component, 'itemtype' => $record->itemtype, 'tagcollid' => $record->tagcollid, 'callback' => $record->callback, 'callbackfile' => $record->callbackfile, 'showstandard' => isset($record->showstandard) ? $record->showstandard : core_tag_tag::BOTH_STANDARD_AND_NOT, 'multiplecontexts' => isset($record->multiplecontexts) ? $record->multiplecontexts : 0)); // Reset cache. cache::make('core', 'tags')->delete('tag_area'); } /** * Update the tag area * * @param stdClass $existing current record from DB table tag_area * @param array|stdClass $data fields that need updating */ public static function update($existing, $data) { global $DB; $data = array_intersect_key((array)$data, array('enabled' => 1, 'tagcollid' => 1, 'callback' => 1, 'callbackfile' => 1, 'showstandard' => 1, 'multiplecontexts' => 1)); foreach ($data as $key => $value) { if ($existing->$key == $value) { unset($data[$key]); } } if (!$data) { return; } if (!empty($data['tagcollid'])) { self::move_tags($existing->component, $existing->itemtype, $data['tagcollid']); } $data['id'] = $existing->id; $DB->update_record('tag_area', $data); // Reset cache. cache::make('core', 'tags')->delete('tag_area'); } /** * Update the database to contain a list of tagged areas for a component. * The list of tagged areas is read from [plugindir]/db/tag.php * * @param string $componentname - The frankenstyle component name. */ public static function reset_definitions_for_component($componentname) { global $DB; $dir = core_component::get_component_directory($componentname); $file = $dir . '/db/tag.php'; $tagareas = null; if (file_exists($file)) { require_once($file); } list($a, $b) = core_component::normalize_component($componentname); $component = $b ? ($a . '_' . $b) : $a; list($existingareas, $existingcolls) = self::get_definitions_for_component($componentname); $itemtypes = array(); $collections = array(); $needcleanup = false; if ($tagareas) { foreach ($tagareas as $tagarea) { $record = (object)$tagarea; if ($component !== 'core' || empty($record->component)) { if (isset($record->component) && $record->component !== $component) { debugging("Item type {$record->itemtype} has illegal component {$record->component}", DEBUG_DEVELOPER); } $record->component = $component; } unset($record->tagcollid); if (!empty($record->collection)) { // Create collection if it does not exist, or update 'searchable' and/or 'customurl' if needed. $key = $record->collection . ':' . $record->component; $collectiondata = array_intersect_key((array)$record, array('component' => 1, 'searchable' => 1, 'customurl' => 1)); $collectiondata['name'] = $record->collection; if (!array_key_exists($key, $existingcolls)) { $existingcolls[$key] = core_tag_collection::create($collectiondata); } else { core_tag_collection::update($existingcolls[$key], $collectiondata); } $record->tagcollid = $existingcolls[$key]->id; $collections[$key] = $existingcolls[$key]; unset($record->collection); } unset($record->searchable); unset($record->customurl); if (!isset($record->callback)) { $record->callback = null; } if (!isset($record->callbackfile)) { $record->callbackfile = null; } if (!isset($record->multiplecontexts)) { $record->multiplecontexts = false; } $itemtypes[$record->itemtype . ':' . $record->component] = $record; } } $todeletearea = array_diff_key($existingareas, $itemtypes); $todeletecoll = array_diff_key($existingcolls, $collections); // Delete tag areas that are no longer needed. foreach ($todeletearea as $key => $record) { self::delete($record); } // Update tag areas if changed. $toupdatearea = array_intersect_key($existingareas, $itemtypes); foreach ($toupdatearea as $key => $tagarea) { if (!isset($itemtypes[$key]->tagcollid)) { foreach ($todeletecoll as $tagcoll) { if ($tagcoll->id == $tagarea->tagcollid) { $itemtypes[$key]->tagcollid = core_tag_collection::get_default(); } } } unset($itemtypes[$key]->showstandard); // Do not override value that was already changed by admin with the default. self::update($tagarea, $itemtypes[$key]); } // Create new tag areas. $toaddarea = array_diff_key($itemtypes, $existingareas); foreach ($toaddarea as $record) { self::create($record); } // Delete tag collections that are no longer needed. foreach ($todeletecoll as $key => $tagcoll) { core_tag_collection::delete($tagcoll); } } /** * Deletes all tag areas, collections and instances associated with the plugin. * * @param string $pluginname */ public static function uninstall($pluginname) { global $DB; list($a, $b) = core_component::normalize_component($pluginname); if (empty($b) || $a === 'core') { throw new coding_exception('Core component can not be uninstalled'); } $component = $a . '_' . $b; core_tag_tag::delete_instances($component); $DB->delete_records('tag_area', array('component' => $component)); $DB->delete_records('tag_coll', array('component' => $component)); cache::make('core', 'tags')->delete_many(array('tag_area', 'tag_coll')); } /** * Moves existing tags associated with an item type to another tag collection * * @param string $component * @param string $itemtype * @param int $tagcollid */ public static function move_tags($component, $itemtype, $tagcollid) { global $DB; $params = array('itemtype1' => $itemtype, 'component1' => $component, 'itemtype2' => $itemtype, 'component2' => $component, 'tagcollid1' => $tagcollid, 'tagcollid2' => $tagcollid); // Find all collections that need to be cleaned later. $sql = "SELECT DISTINCT t.tagcollid " . "FROM {tag_instance} ti " . "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 " . "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 "; $cleanupcollections = $DB->get_fieldset_sql($sql, $params); // Find all tags that are related to the tags being moved and make sure they are present in the target tagcoll. // This query is a little complicated because Oracle does not allow to run SELECT DISTINCT on CLOB fields. $sql = "SELECT name, rawname, description, descriptionformat, userid, isstandard, flag, timemodified ". "FROM {tag} WHERE id IN ". "(SELECT r.id ". "FROM {tag_instance} ti ". // Instances that need moving. "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. "JOIN {tag_instance} tr ON tr.itemtype = 'tag' and tr.component = 'core' AND tr.itemid = t.id ". "JOIN {tag} r ON r.id = tr.tagid ". // Tags related to the tags that need moving. "LEFT JOIN {tag} re ON re.name = r.name AND re.tagcollid = :tagcollid2 ". // Existing tags in the target tagcoll with the same name as related tags. "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". " AND re.id IS NULL)"; // We need related tags that ARE NOT present in the target tagcoll. $result = $DB->get_records_sql($sql, $params); foreach ($result as $tag) { $tag->tagcollid = $tagcollid; $tag->id = $DB->insert_record('tag', $tag); \core\event\tag_created::create_from_tag($tag); } // Find all tags that need moving and have related tags, remember their related tags. $sql = "SELECT t.name AS tagname, r.rawname AS relatedtag ". "FROM {tag_instance} ti ". // Instances that need moving. "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. "JOIN {tag_instance} tr ON t.id = tr.tagid AND tr.itemtype = 'tag' and tr.component = 'core' ". "JOIN {tag} r ON r.id = tr.itemid ". // Tags related to the tags that need moving. "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". "ORDER BY t.id, tr.ordering "; $relatedtags = array(); $result = $DB->get_recordset_sql($sql, $params); foreach ($result as $record) { $relatedtags[$record->tagname][] = $record->relatedtag; } $result->close(); // Find all tags that are used for this itemtype/component and are not present in the target tag collection. // This query is a little complicated because Oracle does not allow to run SELECT DISTINCT on CLOB fields. $sql = "SELECT id, name, rawname, description, descriptionformat, userid, isstandard, flag, timemodified FROM {tag} WHERE id IN (SELECT t.id FROM {tag_instance} ti JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 LEFT JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 AND tt.id IS NULL)"; $movedtags = array(); // Keep track of moved tags so we don't hit DB index violation. $result = $DB->get_records_sql($sql, $params); foreach ($result as $tag) { $originaltagid = $tag->id; if (array_key_exists($tag->name, $movedtags)) { // Case of corrupted data when the same tag was in several collections. $tag->id = $movedtags[$tag->name]; } else { // Copy the tag into the new collection. unset($tag->id); $tag->tagcollid = $tagcollid; $tag->id = $DB->insert_record('tag', $tag); \core\event\tag_created::create_from_tag($tag); $movedtags[$tag->name] = $tag->id; } $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", array($tag->id, $originaltagid, $itemtype, $component)); } // Find all tags that are used for this itemtype/component and are already present in the target tag collection. $sql = "SELECT DISTINCT t.id, tt.id AS targettagid FROM {tag_instance} ti JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 WHERE ti.itemtype = :itemtype2 AND ti.component = :component2"; $result = $DB->get_records_sql($sql, $params); foreach ($result as $tag) { $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", array($tag->targettagid, $tag->id, $itemtype, $component)); } // Add related tags to the moved tags. if ($relatedtags) { $tags = core_tag_tag::get_by_name_bulk($tagcollid, array_keys($relatedtags)); foreach ($tags as $tag) { $tag->add_related_tags($relatedtags[$tag->name]); } } if ($cleanupcollections) { core_tag_collection::cleanup_unused_tags($cleanupcollections); } // Reset caches. cache::make('core', 'tags')->delete('tag_area'); } } tests/privacy_helper.php 0000644 00000005524 15151222237 0011441 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Helpers for the core_tag subsystem implementation of privacy. * * @package core_tag * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\tests; defined('MOODLE_INTERNAL') || die(); use \core_privacy\tests\request\content_writer; global $CFG; /** * Helpers for the core_tag subsystem implementation of privacy. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait privacy_helper { /** * Fetch all tags on a subcontext. * * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @return array */ protected function get_tags_on_subcontext(\context $context, array $subcontext) { $writer = \core_privacy\local\request\writer::with_context($context); return $writer->get_related_data($subcontext, 'tags'); } /** * Check that all tags match on the specified context. * * @param int $userid The ID of the user being stored. * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @param string $component The component being stored. * @param string $itemtype The tag area to store results for. * @param int $itemid The itemid to store. */ protected function assert_all_tags_match_on_context( int $userid, \context $context, array $subcontext, $component, $itemtype, $itemid ) { $writer = \core_privacy\local\request\writer::with_context($context); $dbtags = \core_tag_tag::get_item_tags($component, $itemtype, $itemid); $exportedtags = $this->get_tags_on_subcontext($context, $subcontext); $this->assertCount(count($dbtags), $exportedtags); foreach ($dbtags as $tag) { $this->assertContains($tag->rawname, $exportedtags); } } } manage_table.php 0000644 00000023743 15151222237 0007665 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. use core\output\checkbox_toggleall; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); /** * Class core_tag_manage_table * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_manage_table extends table_sql { /** @var int stores the total number of found tags */ public $totalcount = null; /** @var int */ protected $tagcollid; /** * Constructor * * @param int $tagcollid */ public function __construct($tagcollid) { global $USER, $PAGE, $OUTPUT; parent::__construct('tag-management-list-'.$USER->id); $this->tagcollid = $tagcollid; $perpage = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); $filter = optional_param('filter', '', PARAM_NOTAGS); $baseurl = new moodle_url('/tag/manage.php', array('tc' => $tagcollid, 'perpage' => $perpage, 'page' => $page, 'filter' => $filter)); $checkboxall = new checkbox_toggleall('tags-manage', true, [ 'id' => 'select-all-tags', 'name' => 'select-all-tags', 'checked' => false, 'label' => get_string('selectall'), 'labelclasses' => 'accesshide', ]); $tablecolumns = array('select', 'name', 'fullname', 'count', 'flag', 'timemodified', 'isstandard', 'controls'); $tableheaders = array($OUTPUT->render($checkboxall), get_string('name', 'tag'), get_string('owner', 'tag'), get_string('count', 'tag'), get_string('flag', 'tag'), get_string('timemodified', 'tag'), get_string('standardtag', 'tag'), ''); $this->define_columns($tablecolumns); $this->define_headers($tableheaders); $this->define_baseurl($baseurl); $this->column_class('select', 'mdl-align col-select'); $this->column_class('name', 'col-name'); $this->column_class('owner', 'col-owner'); $this->column_class('count', 'mdl-align col-count'); $this->column_class('flag', 'mdl-align col-flag'); $this->column_class('timemodified', 'col-timemodified'); $this->column_class('isstandard', 'mdl-align col-isstandard'); $this->column_class('controls', 'mdl-align col-controls'); $this->sortable(true, 'flag', SORT_DESC); $this->no_sorting('select'); $this->no_sorting('controls'); $this->set_attribute('cellspacing', '0'); $this->set_attribute('id', 'tag-management-list'); $this->set_attribute('class', 'admintable generaltable tag-management-table'); $totalcount = "SELECT COUNT(tg.id) FROM {tag} tg WHERE tg.tagcollid = :tagcollid"; $params = array('tagcollid' => $this->tagcollid); $this->set_count_sql($totalcount, $params); $this->set_sql('', '', '', $params); $this->collapsible(true); $PAGE->requires->js_call_amd('core/tag', 'initManagePage', array()); } /** * @return string sql to add to where statement. */ function get_sql_where() { $filter = optional_param('filter', '', PARAM_NOTAGS); list($wsql, $wparams) = parent::get_sql_where(); if ($filter !== '') { $wsql .= ($wsql ? ' AND ' : '') . 'tg.name LIKE :tagfilter'; $wparams['tagfilter'] = '%' . $filter . '%'; } return array($wsql, $wparams); } /** * Query the db. Store results in the table object for use by build_table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. Bar * will only be used if there is a fullname column defined for the table. */ public function query_db($pagesize, $useinitialsbar = true) { global $DB; $where = ''; if (!$this->is_downloading()) { $grandtotal = $DB->count_records_sql($this->countsql, $this->countparams); list($wsql, $wparams) = $this->get_sql_where(); if ($wsql) { $this->countsql .= ' AND '.$wsql; $this->countparams = array_merge($this->countparams, $wparams); $where .= ' AND '.$wsql; $this->sql->params = array_merge($this->sql->params, $wparams); $total = $DB->count_records_sql($this->countsql, $this->countparams); } else { $total = $grandtotal; } $this->pagesize(min($pagesize, $total), $total); $this->totalcount = $total; } // Fetch the attempts. $sort = $this->get_sql_sort(); if ($sort) { $sort .= ", tg.name"; } else { $sort = "tg.name"; } $userfieldsapi = \core_user\fields::for_name(); $allusernames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = " SELECT tg.id, tg.name, tg.rawname, tg.isstandard, tg.flag, tg.timemodified, u.id AS owner, $allusernames, COUNT(ti.id) AS count, tg.tagcollid FROM {tag} tg LEFT JOIN {tag_instance} ti ON ti.tagid = tg.id LEFT JOIN {user} u ON u.id = tg.userid WHERE tagcollid = :tagcollid $where GROUP BY tg.id, tg.name, tg.rawname, tg.isstandard, tg.flag, tg.timemodified, u.id, $allusernames, tg.tagcollid ORDER BY $sort"; if (!$this->is_downloading()) { $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); } else { $this->rawdata = $DB->get_records_sql($sql, $this->sql->params); } } /** * Override the table show_hide_link to not show for select column. * * @param string $column the column name, index into various names * @param int $index numerical index of the column * @return string HTML fragment */ protected function show_hide_link($column, $index) { if ($index > 0) { return parent::show_hide_link($column, $index); } return ''; } /** * Get any extra classes names to add to this row in the HTML * * @param stdClass $row array the data for this row. * @return string added to the class="" attribute of the tr. */ public function get_row_class($row) { return $row->flag ? 'table-warning' : ''; } /** * Column name * * @param stdClass $tag * @return string */ public function col_name($tag) { global $OUTPUT; $tagoutput = new core_tag\output\tagname($tag); return $tagoutput->render($OUTPUT); } /** * Column flag * * @param stdClass $tag * @return string */ public function col_flag($tag) { global $OUTPUT; $tagoutput = new core_tag\output\tagflag($tag); return $tagoutput->render($OUTPUT); } /** * Column fullname (user name) * * @param stdClass $tag * @return string */ public function col_fullname($tag) { $params = array('id' => $tag->owner); $ownerlink = new moodle_url('/user/view.php', $params); $owner = html_writer::link($ownerlink, fullname($tag)); return $owner; } /** * Column time modified * * @param stdClass $tag * @return string */ public function col_timemodified($tag) { return format_time(time() - $tag->timemodified); } /** * Column tag type * * @param stdClass $tag * @return string */ public function col_isstandard($tag) { global $OUTPUT; $tagoutput = new core_tag\output\tagisstandard($tag); return $tagoutput->render($OUTPUT); } /** * Column select * * @param stdClass $tag * @return string */ public function col_select($tag) { global $OUTPUT; $checkbox = new checkbox_toggleall('tags-manage', false, [ 'id' => 'tagselect' . $tag->id, 'name' => 'tagschecked[]', 'value' => $tag->id, 'checked' => false, 'label' => get_string('selecttag', 'tag', $tag->rawname), 'labelclasses' => 'accesshide', ]); return $OUTPUT->render($checkbox); } /** * Column controls * * @param stdClass $tag * @return string */ public function col_controls($tag) { global $OUTPUT, $PAGE; $o = ''; // Edit. $url = new moodle_url('/tag/edit.php', array('id' => $tag->id, 'returnurl' => $PAGE->url->out_as_local_url())); $o .= $OUTPUT->action_icon($url, new pix_icon('t/edit', get_string('edittag', 'tag'))); // Delete. $url = new moodle_url($this->baseurl, array('action' => 'delete', 'tagid' => $tag->id, 'sesskey' => sesskey())); $o .= $OUTPUT->action_icon($url, new pix_icon('t/delete', get_string('delete', 'tag')), null, array('class' => 'action-icon tagdelete')); return $o; } } tag.php 0000644 00000221110 15151222237 0006025 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag_tag * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Represents one tag and also contains lots of useful tag-related methods as static functions. * * Tags can be added to any database records. * $itemtype refers to the DB table name * $itemid refers to id field in this DB table * $component is the component that is responsible for the tag instance * $context is the affected context * * BASIC INSTRUCTIONS : * - to "tag a blog post" (for example): * core_tag_tag::set_item_tags('post', 'core', $blogpost->id, $context, $arrayoftags); * * - to "remove all the tags on a blog post": * core_tag_tag::remove_all_item_tags('post', 'core', $blogpost->id); * * set_item_tags() will create tags that do not exist yet. * * @property-read int $id * @property-read string $name * @property-read string $rawname * @property-read int $tagcollid * @property-read int $userid * @property-read int $isstandard * @property-read string $description * @property-read int $descriptionformat * @property-read int $flag 0 if not flagged or positive integer if flagged * @property-read int $timemodified * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_tag { /** @var stdClass data about the tag */ protected $record = null; /** @var int indicates that both standard and not standard tags can be used (or should be returned) */ const BOTH_STANDARD_AND_NOT = 0; /** @var int indicates that only standard tags can be used (or must be returned) */ const STANDARD_ONLY = 1; /** @var int indicates that only non-standard tags should be returned - this does not really have use cases, left for BC */ const NOT_STANDARD_ONLY = -1; /** @var int option to hide standard tags when editing item tags */ const HIDE_STANDARD = 2; /** * Constructor. Use functions get(), get_by_name(), etc. * * @param stdClass $record */ protected function __construct($record) { if (empty($record->id)) { throw new coding_exception("Record must contain at least field 'id'"); } $this->record = $record; } /** * Magic getter * * @param string $name * @return mixed */ public function __get($name) { return $this->record->$name; } /** * Magic isset method * * @param string $name * @return bool */ public function __isset($name) { return isset($this->record->$name); } /** * Converts to object * * @return stdClass */ public function to_object() { return fullclone($this->record); } /** * Returns tag name ready to be displayed * * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string * @return string */ public function get_display_name($ashtml = true) { return static::make_display_name($this->record, $ashtml); } /** * Prepares tag name ready to be displayed * * @param stdClass|core_tag_tag $tag record from db table tag, must contain properties name and rawname * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string * @return string */ public static function make_display_name($tag, $ashtml = true) { global $CFG; if (empty($CFG->keeptagnamecase)) { // This is the normalized tag name. $tagname = core_text::strtotitle($tag->name); } else { // Original casing of the tag name. $tagname = $tag->rawname; } // Clean up a bit just in case the rules change again. $tagname = clean_param($tagname, PARAM_TAG); return $ashtml ? htmlspecialchars($tagname, ENT_COMPAT) : $tagname; } /** * Adds one or more tag in the database. This function should not be called directly : you should * use tag_set. * * @param int $tagcollid * @param string|array $tags one tag, or an array of tags, to be created * @param bool $isstandard type of tag to be created. A standard tag is kept even if there are no records tagged with it. * @return array tag objects indexed by their lowercase normalized names. Any boolean false in the array * indicates an error while adding the tag. */ protected static function add($tagcollid, $tags, $isstandard = false) { global $USER, $DB; $tagobject = new stdClass(); $tagobject->isstandard = $isstandard ? 1 : 0; $tagobject->userid = $USER->id; $tagobject->timemodified = time(); $tagobject->tagcollid = $tagcollid; $rv = array(); foreach ($tags as $veryrawname) { $rawname = clean_param($veryrawname, PARAM_TAG); if (!$rawname) { $rv[$rawname] = false; } else { $obj = (object)(array)$tagobject; $obj->rawname = $rawname; $obj->name = core_text::strtolower($rawname); $obj->id = $DB->insert_record('tag', $obj); $rv[$obj->name] = new static($obj); \core\event\tag_created::create_from_tag($rv[$obj->name])->trigger(); } } return $rv; } /** * Simple function to just return a single tag object by its id * * @param int $id * @param string $returnfields which fields do we want returned from table {tag}. * Default value is 'id,name,rawname,tagcollid', * specify '*' to include all fields. * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; * IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended); * MUST_EXIST means throw exception if no record or multiple records found * @return core_tag_tag|false tag object */ public static function get($id, $returnfields = 'id, name, rawname, tagcollid', $strictness = IGNORE_MISSING) { global $DB; $record = $DB->get_record('tag', array('id' => $id), $returnfields, $strictness); if ($record) { return new static($record); } return false; } /** * Simple function to just return an array of tag objects by their ids * * @param int[] $ids * @param string $returnfields which fields do we want returned from table {tag}. * Default value is 'id,name,rawname,tagcollid', * specify '*' to include all fields. * @return core_tag_tag[] array of retrieved tags */ public static function get_bulk($ids, $returnfields = 'id, name, rawname, tagcollid') { global $DB; $result = array(); if (empty($ids)) { return $result; } list($sql, $params) = $DB->get_in_or_equal($ids); $records = $DB->get_records_select('tag', 'id '.$sql, $params, '', $returnfields); foreach ($records as $record) { $result[$record->id] = new static($record); } return $result; } /** * Simple function to just return a single tag object by tagcollid and name * * @param int $tagcollid tag collection to use, * if 0 is given we will try to guess the tag collection and return the first match * @param string $name tag name * @param string $returnfields which fields do we want returned. This is a comma separated string * containing any combination of 'id', 'name', 'rawname', 'tagcollid' or '*' to include all fields. * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; * IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended); * MUST_EXIST means throw exception if no record or multiple records found * @return core_tag_tag|false tag object */ public static function get_by_name($tagcollid, $name, $returnfields='id, name, rawname, tagcollid', $strictness = IGNORE_MISSING) { global $DB; if ($tagcollid == 0) { $tags = static::guess_by_name($name, $returnfields); if ($tags) { $tag = reset($tags); return $tag; } else if ($strictness == MUST_EXIST) { throw new dml_missing_record_exception('tag', 'name=?', array($name)); } return false; } $name = core_text::strtolower($name); // To cope with input that might just be wrong case. $params = array('name' => $name, 'tagcollid' => $tagcollid); $record = $DB->get_record('tag', $params, $returnfields, $strictness); if ($record) { return new static($record); } return false; } /** * Looking in all tag collections for the tag with the given name * * @param string $name tag name * @param string $returnfields * @return array array of core_tag_tag instances */ public static function guess_by_name($name, $returnfields='id, name, rawname, tagcollid') { global $DB; if (empty($name)) { return array(); } $tagcolls = core_tag_collection::get_collections(); list($sql, $params) = $DB->get_in_or_equal(array_keys($tagcolls), SQL_PARAMS_NAMED); $params['name'] = core_text::strtolower($name); $tags = $DB->get_records_select('tag', 'name = :name AND tagcollid ' . $sql, $params, '', $returnfields); if (count($tags) > 1) { // Sort in the same order as tag collections. $tagcolls = core_tag_collection::get_collections(); uasort($tags, function($a, $b) use ($tagcolls) { return $tagcolls[$a->tagcollid]->sortorder < $tagcolls[$b->tagcollid]->sortorder ? -1 : 1; }); } $rv = array(); foreach ($tags as $id => $tag) { $rv[$id] = new static($tag); } return $rv; } /** * Returns the list of tag objects by tag collection id and the list of tag names * * @param int $tagcollid * @param array $tags array of tags to look for * @param string $returnfields list of DB fields to return, must contain 'id', 'name' and 'rawname' * @return array tag-indexed array of objects. No value for a key means the tag wasn't found. */ public static function get_by_name_bulk($tagcollid, $tags, $returnfields = 'id, name, rawname, tagcollid') { global $DB; if (empty($tags)) { return array(); } $cleantags = self::normalize(self::normalize($tags, false)); // Format: rawname => normalised name. list($namesql, $params) = $DB->get_in_or_equal(array_values($cleantags)); array_unshift($params, $tagcollid); $recordset = $DB->get_recordset_sql("SELECT $returnfields FROM {tag} WHERE tagcollid = ? AND name $namesql", $params); $result = array_fill_keys($cleantags, null); foreach ($recordset as $record) { $result[$record->name] = new static($record); } $recordset->close(); return $result; } /** * Function that normalizes a list of tag names. * * @param array $rawtags array of tags * @param bool $tolowercase convert to lower case? * @return array lowercased normalized tags, indexed by the normalized tag, in the same order as the original array. * (Eg: 'Banana' => 'banana'). */ public static function normalize($rawtags, $tolowercase = true) { $result = array(); foreach ($rawtags as $rawtag) { $rawtag = trim($rawtag); if (strval($rawtag) !== '') { $clean = clean_param($rawtag, PARAM_TAG); if ($tolowercase) { $result[$rawtag] = core_text::strtolower($clean); } else { $result[$rawtag] = $clean; } } } return $result; } /** * Retrieves tags and/or creates them if do not exist yet * * @param int $tagcollid * @param array $tags array of raw tag names, do not have to be normalised * @param bool $isstandard create as standard tag (default false) * @return core_tag_tag[] array of tag objects indexed with lowercase normalised tag name */ public static function create_if_missing($tagcollid, $tags, $isstandard = false) { $cleantags = self::normalize(array_filter(self::normalize($tags, false))); // Array rawname => normalised name . $result = static::get_by_name_bulk($tagcollid, $tags, '*'); $existing = array_filter($result); $missing = array_diff_key(array_flip($cleantags), $existing); // Array normalised name => rawname. if ($missing) { $newtags = static::add($tagcollid, array_values($missing), $isstandard); foreach ($newtags as $tag) { $result[$tag->name] = $tag; } } return $result; } /** * Creates a URL to view a tag * * @param int $tagcollid * @param string $name * @param int $exclusivemode * @param int $fromctx context id where this tag cloud is displayed * @param int $ctx context id for tag view link * @param int $rec recursive argument for tag view link * @return \moodle_url */ public static function make_url($tagcollid, $name, $exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) { $coll = core_tag_collection::get_by_id($tagcollid); if (!empty($coll->customurl)) { $url = '/' . ltrim(trim($coll->customurl), '/'); } else { $url = '/tag/index.php'; } $params = array('tc' => $tagcollid, 'tag' => $name); if ($exclusivemode) { $params['excl'] = 1; } if ($fromctx) { $params['from'] = $fromctx; } if ($ctx) { $params['ctx'] = $ctx; } if (!$rec) { $params['rec'] = 0; } return new moodle_url($url, $params); } /** * Returns URL to view the tag * * @param int $exclusivemode * @param int $fromctx context id where this tag cloud is displayed * @param int $ctx context id for tag view link * @param int $rec recursive argument for tag view link * @return \moodle_url */ public function get_view_url($exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) { return static::make_url($this->record->tagcollid, $this->record->rawname, $exclusivemode, $fromctx, $ctx, $rec); } /** * Validates that the required fields were retrieved and retrieves them if missing * * @param array $list array of the fields that need to be validated * @param string $caller name of the function that requested it, for the debugging message */ protected function ensure_fields_exist($list, $caller) { global $DB; $missing = array_diff($list, array_keys((array)$this->record)); if ($missing) { debugging('core_tag_tag::' . $caller . '() must be called on fully retrieved tag object. Missing fields: '. join(', ', $missing), DEBUG_DEVELOPER); $this->record = $DB->get_record('tag', array('id' => $this->record->id), '*', MUST_EXIST); } } /** * Deletes the tag instance given the record from tag_instance DB table * * @param stdClass $taginstance * @param bool $fullobject whether $taginstance contains all fields from DB table tag_instance * (in this case it is safe to add a record snapshot to the event) * @return bool */ protected function delete_instance_as_record($taginstance, $fullobject = false) { global $DB; $this->ensure_fields_exist(array('name', 'rawname', 'isstandard'), 'delete_instance_as_record'); $DB->delete_records('tag_instance', array('id' => $taginstance->id)); // We can not fire an event with 'null' as the contextid. if (is_null($taginstance->contextid)) { $taginstance->contextid = context_system::instance()->id; } // Trigger tag removed event. $taginstance->tagid = $this->id; \core\event\tag_removed::create_from_tag_instance($taginstance, $this->name, $this->rawname, $fullobject)->trigger(); // If there are no other instances of the tag then consider deleting the tag as well. if (!$this->isstandard) { if (!$DB->record_exists('tag_instance', array('tagid' => $this->id))) { self::delete_tags($this->id); } } return true; } /** * Delete one instance of a tag. If the last instance was deleted, it will also delete the tag, unless it is standard. * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype the type of the record for which to remove the instance * @param int $itemid the id of the record for which to remove the instance * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) */ protected function delete_instance($component, $itemtype, $itemid, $tiuserid = 0) { global $DB; $params = array('tagid' => $this->id, 'itemtype' => $itemtype, 'itemid' => $itemid); if ($tiuserid) { $params['tiuserid'] = $tiuserid; } if ($component) { $params['component'] = $component; } $taginstance = $DB->get_record('tag_instance', $params); if (!$taginstance) { return; } $this->delete_instance_as_record($taginstance, true); } /** * Bulk delete all tag instances. * * @param stdClass[] $taginstances A list of tag_instance records to delete. Each * record must also contain the name and rawname * columns from the related tag record. */ public static function delete_instances_as_record(array $taginstances) { global $DB; if (empty($taginstances)) { return; } $taginstanceids = array_map(function($taginstance) { return $taginstance->id; }, $taginstances); // Now remove all the tag instances. $DB->delete_records_list('tag_instance', 'id', $taginstanceids); // Save the system context in case the 'contextid' column in the 'tag_instance' table is null. $syscontextid = context_system::instance()->id; // Loop through the tag instances and fire an 'tag_removed' event. foreach ($taginstances as $taginstance) { // We can not fire an event with 'null' as the contextid. if (is_null($taginstance->contextid)) { $taginstance->contextid = $syscontextid; } // Trigger tag removed event. \core\event\tag_removed::create_from_tag_instance($taginstance, $taginstance->name, $taginstance->rawname, true)->trigger(); } } /** * Bulk delete all tag instances by tag id. * * @param int[] $taginstanceids List of tag instance ids to be deleted. */ public static function delete_instances_by_id(array $taginstanceids) { global $DB; if (empty($taginstanceids)) { return; } list($idsql, $params) = $DB->get_in_or_equal($taginstanceids); $sql = "SELECT ti.*, t.name, t.rawname, t.isstandard FROM {tag_instance} ti JOIN {tag} t ON ti.tagid = t.id WHERE ti.id {$idsql}"; if ($taginstances = $DB->get_records_sql($sql, $params)) { static::delete_instances_as_record($taginstances); } } /** * Bulk delete all tag instances for a component or tag area * * @param string $component * @param string $itemtype (optional) * @param int $contextid (optional) */ public static function delete_instances($component, $itemtype = null, $contextid = null) { global $DB; $sql = "SELECT ti.*, t.name, t.rawname, t.isstandard FROM {tag_instance} ti JOIN {tag} t ON ti.tagid = t.id WHERE ti.component = :component"; $params = array('component' => $component); if (!is_null($contextid)) { $sql .= " AND ti.contextid = :contextid"; $params['contextid'] = $contextid; } if (!is_null($itemtype)) { $sql .= " AND ti.itemtype = :itemtype"; $params['itemtype'] = $itemtype; } if ($taginstances = $DB->get_records_sql($sql, $params)) { static::delete_instances_as_record($taginstances); } } /** * Adds a tag instance * * @param string $component * @param string $itemtype * @param string $itemid * @param context $context * @param int $ordering * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) * @return int id of tag_instance */ protected function add_instance($component, $itemtype, $itemid, context $context, $ordering, $tiuserid = 0) { global $DB; $this->ensure_fields_exist(array('name', 'rawname'), 'add_instance'); $taginstance = new stdClass; $taginstance->tagid = $this->id; $taginstance->component = $component ? $component : ''; $taginstance->itemid = $itemid; $taginstance->itemtype = $itemtype; $taginstance->contextid = $context->id; $taginstance->ordering = $ordering; $taginstance->timecreated = time(); $taginstance->timemodified = $taginstance->timecreated; $taginstance->tiuserid = $tiuserid; $taginstance->id = $DB->insert_record('tag_instance', $taginstance); // Trigger tag added event. \core\event\tag_added::create_from_tag_instance($taginstance, $this->name, $this->rawname, true)->trigger(); return $taginstance->id; } /** * Updates the ordering on tag instance * * @param int $instanceid * @param int $ordering */ protected function update_instance_ordering($instanceid, $ordering) { global $DB; $data = new stdClass(); $data->id = $instanceid; $data->ordering = $ordering; $data->timemodified = time(); $DB->update_record('tag_instance', $data); } /** * Get the array of core_tag_tag objects associated with a list of items. * * Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array. * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype type of the tagged item * @param int[] $itemids * @param int $standardonly wether to return only standard tags or any * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging * @return core_tag_tag[][] first array key is itemid. For each itemid, * an array tagid => tag object with additional fields taginstanceid, taginstancecontextid and ordering */ public static function get_items_tags($component, $itemtype, $itemids, $standardonly = self::BOTH_STANDARD_AND_NOT, $tiuserid = 0) { global $DB; if (static::is_enabled($component, $itemtype) === false) { // Tagging area is properly defined but not enabled - return empty array. return array(); } if (empty($itemids)) { return array(); } $standardonly = (int)$standardonly; // In case somebody passed bool. list($idsql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); // Note: if the fields in this query are changed, you need to do the same changes in core_tag_tag::get_correlated_tags(). $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.isstandard, tg.name, tg.rawname, tg.flag, tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid, ti.itemid FROM {tag_instance} ti JOIN {tag} tg ON tg.id = ti.tagid WHERE ti.itemtype = :itemtype AND ti.itemid $idsql ". ($component ? "AND ti.component = :component " : ""). ($tiuserid ? "AND ti.tiuserid = :tiuserid " : ""). (($standardonly == self::STANDARD_ONLY) ? "AND tg.isstandard = 1 " : ""). (($standardonly == self::NOT_STANDARD_ONLY) ? "AND tg.isstandard = 0 " : ""). "ORDER BY ti.ordering ASC, ti.id"; $params['itemtype'] = $itemtype; $params['component'] = $component; $params['tiuserid'] = $tiuserid; $records = $DB->get_records_sql($sql, $params); $result = array(); foreach ($itemids as $itemid) { $result[$itemid] = []; } foreach ($records as $id => $record) { $result[$record->itemid][$id] = new static($record); } return $result; } /** * Get the array of core_tag_tag objects associated with an item (instances). * * Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array. * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype type of the tagged item * @param int $itemid * @param int $standardonly wether to return only standard tags or any * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging * @return core_tag_tag[] each object contains additional fields taginstanceid, taginstancecontextid and ordering */ public static function get_item_tags($component, $itemtype, $itemid, $standardonly = self::BOTH_STANDARD_AND_NOT, $tiuserid = 0) { $tagobjects = static::get_items_tags($component, $itemtype, [$itemid], $standardonly, $tiuserid); return empty($tagobjects) ? [] : $tagobjects[$itemid]; } /** * Returns the list of display names of the tags that are associated with an item * * This method is usually used to prefill the form data for the 'tags' form element * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype type of the tagged item * @param int $itemid * @param int $standardonly wether to return only standard tags or any * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging * @param bool $ashtml (default true) if true will return htmlspecialchars encoded tag names * @return string[] array of tags display names */ public static function get_item_tags_array($component, $itemtype, $itemid, $standardonly = self::BOTH_STANDARD_AND_NOT, $tiuserid = 0, $ashtml = true) { $tags = array(); foreach (static::get_item_tags($component, $itemtype, $itemid, $standardonly, $tiuserid) as $tag) { $tags[$tag->id] = $tag->get_display_name($ashtml); } return $tags; } /** * Sets the list of tag instances for one item (table record). * * Extra exsisting instances are removed, new ones are added. New tags are created if needed. * * This method can not be used for setting tags relations, please use set_related_tags() * * @param string $component component responsible for tagging * @param string $itemtype type of the tagged item * @param int $itemid * @param context $context * @param array $tagnames * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) */ public static function set_item_tags($component, $itemtype, $itemid, context $context, $tagnames, $tiuserid = 0) { if ($itemtype === 'tag') { if ($tiuserid) { throw new coding_exception('Related tags can not have tag instance userid'); } debugging('You can not use set_item_tags() for tagging a tag, please use set_related_tags()', DEBUG_DEVELOPER); static::get($itemid, '*', MUST_EXIST)->set_related_tags($tagnames); return; } if ($tagnames !== null && static::is_enabled($component, $itemtype) === false) { // Tagging area is properly defined but not enabled - do nothing. // Unless we are deleting the item tags ($tagnames === null), in which case proceed with deleting. return; } // Apply clean_param() to all tags. if ($tagnames) { $tagcollid = core_tag_area::get_collection($component, $itemtype); $tagobjects = static::create_if_missing($tagcollid, $tagnames); } else { $tagobjects = array(); } $allowmultiplecontexts = core_tag_area::allows_tagging_in_multiple_contexts($component, $itemtype); $currenttags = static::get_item_tags($component, $itemtype, $itemid, self::BOTH_STANDARD_AND_NOT, $tiuserid); $taginstanceidstomovecontext = []; // For data coherence reasons, it's better to remove deleted tags // before adding new data: ordering could be duplicated. foreach ($currenttags as $currenttag) { $hasbeenrequested = array_key_exists($currenttag->name, $tagobjects); $issamecontext = $currenttag->taginstancecontextid == $context->id; if ($allowmultiplecontexts) { // If the tag area allows multiple contexts then we should only be // managing tags in the given $context. All other tags can be ignored. $shoulddelete = $issamecontext && !$hasbeenrequested; } else { // If the tag area only allows tag instances in a single context then // all tags that aren't in the requested tags should be deleted, regardless // of their context, if they are not part of the new set of tags. $shoulddelete = !$hasbeenrequested; // If the tag instance isn't in the correct context (legacy data) // then we should take this opportunity to update it with the correct // context id. if (!$shoulddelete && !$issamecontext) { $currenttag->taginstancecontextid = $context->id; $taginstanceidstomovecontext[] = $currenttag->taginstanceid; } } if ($shoulddelete) { $taginstance = (object)array('id' => $currenttag->taginstanceid, 'itemtype' => $itemtype, 'itemid' => $itemid, 'contextid' => $currenttag->taginstancecontextid, 'tiuserid' => $tiuserid); $currenttag->delete_instance_as_record($taginstance, false); } } if (!empty($taginstanceidstomovecontext)) { static::change_instances_context($taginstanceidstomovecontext, $context); } $ordering = -1; foreach ($tagobjects as $name => $tag) { $ordering++; foreach ($currenttags as $currenttag) { $namesmatch = strval($currenttag->name) === strval($name); if ($allowmultiplecontexts) { // If the tag area allows multiple contexts then we should only // skip adding a new instance if the existing one is in the correct // context. $contextsmatch = $currenttag->taginstancecontextid == $context->id; $shouldskipinstance = $namesmatch && $contextsmatch; } else { // The existing behaviour for single context tag areas is to // skip adding a new instance regardless of whether the existing // instance is in the same context as the provided $context. $shouldskipinstance = $namesmatch; } if ($shouldskipinstance) { if ($currenttag->ordering != $ordering) { $currenttag->update_instance_ordering($currenttag->taginstanceid, $ordering); } continue 2; } } $tag->add_instance($component, $itemtype, $itemid, $context, $ordering, $tiuserid); } } /** * Removes all tags from an item. * * All tags will be removed even if tagging is disabled in this area. This is * usually called when the item itself has been deleted. * * @param string $component component responsible for tagging * @param string $itemtype type of the tagged item * @param int $itemid * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) */ public static function remove_all_item_tags($component, $itemtype, $itemid, $tiuserid = 0) { $context = context_system::instance(); // Context will not be used. static::set_item_tags($component, $itemtype, $itemid, $context, null, $tiuserid); } /** * Adds a tag to an item, without overwriting the current tags. * * If the tag has already been added to the record, no changes are made. * * @param string $component the component that was tagged * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) * @param int $itemid the id of the record to tag * @param context $context the context of where this tag was assigned * @param string $tagname the tag to add * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) * @return int id of tag_instance that was either created or already existed or null if tagging is not enabled */ public static function add_item_tag($component, $itemtype, $itemid, context $context, $tagname, $tiuserid = 0) { global $DB; if (static::is_enabled($component, $itemtype) === false) { // Tagging area is properly defined but not enabled - do nothing. return null; } $rawname = clean_param($tagname, PARAM_TAG); $normalisedname = core_text::strtolower($rawname); $tagcollid = core_tag_area::get_collection($component, $itemtype); $usersql = $tiuserid ? " AND ti.tiuserid = :tiuserid " : ""; $sql = 'SELECT t.*, ti.id AS taginstanceid FROM {tag} t LEFT JOIN {tag_instance} ti ON ti.tagid = t.id AND ti.itemtype = :itemtype '. $usersql . 'AND ti.itemid = :itemid AND ti.component = :component WHERE t.name = :name AND t.tagcollid = :tagcollid'; $params = array('name' => $normalisedname, 'tagcollid' => $tagcollid, 'itemtype' => $itemtype, 'itemid' => $itemid, 'component' => $component, 'tiuserid' => $tiuserid); $record = $DB->get_record_sql($sql, $params); if ($record) { if ($record->taginstanceid) { // Tag was already added to the item, nothing to do here. return $record->taginstanceid; } $tag = new static($record); } else { // The tag does not exist yet, create it. $tags = static::add($tagcollid, array($tagname)); $tag = reset($tags); } $ordering = $DB->get_field_sql('SELECT MAX(ordering) FROM {tag_instance} ti WHERE ti.itemtype = :itemtype AND ti.itemid = :itemid AND ti.component = :component' . $usersql, $params); return $tag->add_instance($component, $itemtype, $itemid, $context, $ordering + 1, $tiuserid); } /** * Removes the tag from an item without changing the other tags * * @param string $component the component that was tagged * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) * @param int $itemid the id of the record to tag * @param string $tagname the tag to remove * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course) */ public static function remove_item_tag($component, $itemtype, $itemid, $tagname, $tiuserid = 0) { global $DB; if (static::is_enabled($component, $itemtype) === false) { // Tagging area is properly defined but not enabled - do nothing. return array(); } $rawname = clean_param($tagname, PARAM_TAG); $normalisedname = core_text::strtolower($rawname); $usersql = $tiuserid ? " AND tiuserid = :tiuserid " : ""; $componentsql = $component ? " AND ti.component = :component " : ""; $sql = 'SELECT t.*, ti.id AS taginstanceid, ti.contextid AS taginstancecontextid, ti.ordering FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id ' . $usersql . ' WHERE t.name = :name AND ti.itemtype = :itemtype AND ti.itemid = :itemid ' . $componentsql; $params = array('name' => $normalisedname, 'itemtype' => $itemtype, 'itemid' => $itemid, 'component' => $component, 'tiuserid' => $tiuserid); if ($record = $DB->get_record_sql($sql, $params)) { $taginstance = (object)array('id' => $record->taginstanceid, 'itemtype' => $itemtype, 'itemid' => $itemid, 'contextid' => $record->taginstancecontextid, 'tiuserid' => $tiuserid); $tag = new static($record); $tag->delete_instance_as_record($taginstance, false); $componentsql = $component ? " AND component = :component " : ""; $sql = "UPDATE {tag_instance} SET ordering = ordering - 1 WHERE itemtype = :itemtype AND itemid = :itemid $componentsql $usersql AND ordering > :ordering"; $params['ordering'] = $record->ordering; $DB->execute($sql, $params); } } /** * Allows to move all tag instances from one context to another * * @param string $component the component that was tagged * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) * @param context $oldcontext * @param context $newcontext */ public static function move_context($component, $itemtype, $oldcontext, $newcontext) { global $DB; if ($oldcontext instanceof context) { $oldcontext = $oldcontext->id; } if ($newcontext instanceof context) { $newcontext = $newcontext->id; } $DB->set_field('tag_instance', 'contextid', $newcontext, array('component' => $component, 'itemtype' => $itemtype, 'contextid' => $oldcontext)); } /** * Moves all tags of the specified items to the new context * * @param string $component the component that was tagged * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.) * @param array $itemids * @param context|int $newcontext target context to move tags to */ public static function change_items_context($component, $itemtype, $itemids, $newcontext) { global $DB; if (empty($itemids)) { return; } if (!is_array($itemids)) { $itemids = array($itemids); } list($sql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); $params['component'] = $component; $params['itemtype'] = $itemtype; if ($newcontext instanceof context) { $newcontext = $newcontext->id; } $DB->set_field_select('tag_instance', 'contextid', $newcontext, 'component = :component AND itemtype = :itemtype AND itemid ' . $sql, $params); } /** * Moves all of the specified tag instances into a new context. * * @param array $taginstanceids The list of tag instance ids that should be moved * @param context $newcontext The context to move the tag instances into */ public static function change_instances_context(array $taginstanceids, context $newcontext) { global $DB; if (empty($taginstanceids)) { return; } list($sql, $params) = $DB->get_in_or_equal($taginstanceids); $DB->set_field_select('tag_instance', 'contextid', $newcontext->id, "id {$sql}", $params); } /** * Updates the information about the tag * * @param array|stdClass $data data to update, may contain: isstandard, description, descriptionformat, rawname * @return bool whether the tag was updated. False may be returned if: all new values match the existing, * or it was attempted to rename the tag to the name that is already used. */ public function update($data) { global $DB, $COURSE; $allowedfields = array('isstandard', 'description', 'descriptionformat', 'rawname'); $data = (array)$data; if ($extrafields = array_diff(array_keys($data), $allowedfields)) { debugging('The field(s) '.join(', ', $extrafields).' will be ignored when updating the tag', DEBUG_DEVELOPER); } $data = array_intersect_key($data, array_fill_keys($allowedfields, 1)); $this->ensure_fields_exist(array_merge(array('tagcollid', 'userid', 'name', 'rawname'), array_keys($data)), 'update'); // Validate the tag name. if (array_key_exists('rawname', $data)) { $data['rawname'] = clean_param($data['rawname'], PARAM_TAG); $name = core_text::strtolower($data['rawname']); if (!$name || $data['rawname'] === $this->rawname) { unset($data['rawname']); } else if ($existing = static::get_by_name($this->tagcollid, $name, 'id')) { // Prevent the rename if a tag with that name already exists. if ($existing->id != $this->id) { throw new moodle_exception('namesalreadybeeingused', 'core_tag'); } } if (isset($data['rawname'])) { $data['name'] = $name; } } // Validate the tag type. if (array_key_exists('isstandard', $data)) { $data['isstandard'] = $data['isstandard'] ? 1 : 0; } // Find only the attributes that need to be changed. $originalname = $this->name; foreach ($data as $key => $value) { if ($this->record->$key !== $value) { $this->record->$key = $value; } else { unset($data[$key]); } } if (empty($data)) { return false; } $data['id'] = $this->id; $data['timemodified'] = time(); $DB->update_record('tag', $data); $event = \core\event\tag_updated::create(array( 'objectid' => $this->id, 'relateduserid' => $this->userid, 'context' => context_system::instance(), 'other' => array( 'name' => $this->name, 'rawname' => $this->rawname ) )); if (isset($data['rawname'])) { $event->set_legacy_logdata(array($COURSE->id, 'tag', 'update', 'index.php?id='. $this->id, $originalname . '->'. $this->name)); } $event->trigger(); return true; } /** * Flag a tag as inappropriate */ public function flag() { global $DB; $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag'); // Update all the tags to flagged. $this->timemodified = time(); $this->flag++; $DB->update_record('tag', array('timemodified' => $this->timemodified, 'flag' => $this->flag, 'id' => $this->id)); $event = \core\event\tag_flagged::create(array( 'objectid' => $this->id, 'relateduserid' => $this->userid, 'context' => context_system::instance(), 'other' => array( 'name' => $this->name, 'rawname' => $this->rawname ) )); $event->trigger(); } /** * Remove the inappropriate flag on a tag. */ public function reset_flag() { global $DB; $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag'); if (!$this->flag) { // Nothing to do. return false; } $this->timemodified = time(); $this->flag = 0; $DB->update_record('tag', array('timemodified' => $this->timemodified, 'flag' => 0, 'id' => $this->id)); $event = \core\event\tag_unflagged::create(array( 'objectid' => $this->id, 'relateduserid' => $this->userid, 'context' => context_system::instance(), 'other' => array( 'name' => $this->name, 'rawname' => $this->rawname ) )); $event->trigger(); } /** * Sets the list of tags related to this one. * * Tag relations are recorded by two instances linking two tags to each other. * For tag relations ordering is not used and may be random. * * @param array $tagnames */ public function set_related_tags($tagnames) { $context = context_system::instance(); $tagobjects = $tagnames ? static::create_if_missing($this->tagcollid, $tagnames) : array(); unset($tagobjects[$this->name]); // Never link to itself. $currenttags = static::get_item_tags('core', 'tag', $this->id); // For data coherence reasons, it's better to remove deleted tags // before adding new data: ordering could be duplicated. foreach ($currenttags as $currenttag) { if (!array_key_exists($currenttag->name, $tagobjects)) { $taginstance = (object)array('id' => $currenttag->taginstanceid, 'itemtype' => 'tag', 'itemid' => $this->id, 'contextid' => $context->id); $currenttag->delete_instance_as_record($taginstance, false); $this->delete_instance('core', 'tag', $currenttag->id); } } foreach ($tagobjects as $name => $tag) { foreach ($currenttags as $currenttag) { if ($currenttag->name === $name) { continue 2; } } $this->add_instance('core', 'tag', $tag->id, $context, 0); $tag->add_instance('core', 'tag', $this->id, $context, 0); $currenttags[] = $tag; } } /** * Adds to the list of related tags without removing existing * * Tag relations are recorded by two instances linking two tags to each other. * For tag relations ordering is not used and may be random. * * @param array $tagnames */ public function add_related_tags($tagnames) { $context = context_system::instance(); $tagobjects = static::create_if_missing($this->tagcollid, $tagnames); $currenttags = static::get_item_tags('core', 'tag', $this->id); foreach ($tagobjects as $name => $tag) { foreach ($currenttags as $currenttag) { if ($currenttag->name === $name) { continue 2; } } $this->add_instance('core', 'tag', $tag->id, $context, 0); $tag->add_instance('core', 'tag', $this->id, $context, 0); $currenttags[] = $tag; } } /** * Returns the correlated tags of a tag, retrieved from the tag_correlation table. * * Correlated tags are calculated in cron based on existing tag instances. * * @param bool $keepduplicates if true, will return one record for each existing * tag instance which may result in duplicates of the actual tags * @return core_tag_tag[] an array of tag objects */ public function get_correlated_tags($keepduplicates = false) { global $DB; $correlated = $DB->get_field('tag_correlation', 'correlatedtags', array('tagid' => $this->id)); if (!$correlated) { return array(); } $correlated = preg_split('/\s*,\s*/', trim($correlated), -1, PREG_SPLIT_NO_EMPTY); list($query, $params) = $DB->get_in_or_equal($correlated); // This is (and has to) return the same fields as the query in core_tag_tag::get_item_tags(). $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.isstandard, tg.name, tg.rawname, tg.flag, tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid, ti.itemid FROM {tag} tg INNER JOIN {tag_instance} ti ON tg.id = ti.tagid WHERE tg.id $query AND tg.id <> ? AND tg.tagcollid = ? ORDER BY ti.ordering ASC, ti.id"; $params[] = $this->id; $params[] = $this->tagcollid; $records = $DB->get_records_sql($sql, $params); $seen = array(); $result = array(); foreach ($records as $id => $record) { if (!$keepduplicates && !empty($seen[$record->id])) { continue; } $result[$id] = new static($record); $seen[$record->id] = true; } return $result; } /** * Returns tags that this tag was manually set as related to * * @return core_tag_tag[] */ public function get_manual_related_tags() { return self::get_item_tags('core', 'tag', $this->id); } /** * Returns tags related to a tag * * Related tags of a tag come from two sources: * - manually added related tags, which are tag_instance entries for that tag * - correlated tags, which are calculated * * @return core_tag_tag[] an array of tag objects */ public function get_related_tags() { $manual = $this->get_manual_related_tags(); $automatic = $this->get_correlated_tags(); $relatedtags = array_merge($manual, $automatic); // Remove duplicated tags (multiple instances of the same tag). $seen = array(); foreach ($relatedtags as $instance => $tag) { if (isset($seen[$tag->id])) { unset($relatedtags[$instance]); } else { $seen[$tag->id] = 1; } } return $relatedtags; } /** * Find all items tagged with a tag of a given type ('post', 'user', etc.) * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype type to restrict search to * @param int $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point. * @param int $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records. * @param string $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it' * @param array $params additional parameters for the DB query * @return array of matching objects, indexed by record id, from the table containing the type requested */ public function get_tagged_items($component, $itemtype, $limitfrom = '', $limitnum = '', $subquery = '', $params = array()) { global $DB; if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) { return array(); } $params = $params ? $params : array(); $query = "SELECT it.* FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid"; $params['itemtype'] = $itemtype; $params['tagid'] = $this->id; if ($component) { $query .= ' AND tt.component = :component'; $params['component'] = $component; } if ($subquery) { $query .= ' AND ' . $subquery; } $query .= ' ORDER BY it.id'; return $DB->get_records_sql($query, $params, $limitfrom, $limitnum); } /** * Count how many items are tagged with a specific tag. * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype type to restrict search to * @param string $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it' * @param array $params additional parameters for the DB query * @return int number of mathing tags. */ public function count_tagged_items($component, $itemtype, $subquery = '', $params = array()) { global $DB; if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) { return 0; } $params = $params ? $params : array(); $query = "SELECT COUNT(it.id) FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid"; $params['itemtype'] = $itemtype; $params['tagid'] = $this->id; if ($component) { $query .= ' AND tt.component = :component'; $params['component'] = $component; } if ($subquery) { $query .= ' AND ' . $subquery; } return $DB->get_field_sql($query, $params); } /** * Determine if an item is tagged with a specific tag * * Note that this is a static method and not a method of core_tag object because the tag might not exist yet, * for example user searches for "php" and we offer him to add "php" to his interests. * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype the record type to look for * @param int $itemid the record id to look for * @param string $tagname a tag name * @return int 1 if it is tagged, 0 otherwise */ public static function is_item_tagged_with($component, $itemtype, $itemid, $tagname) { global $DB; $tagcollid = core_tag_area::get_collection($component, $itemtype); $query = 'SELECT 1 FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE t.name = ? AND t.tagcollid = ? AND ti.itemtype = ? AND ti.itemid = ?'; $cleanname = core_text::strtolower(clean_param($tagname, PARAM_TAG)); $params = array($cleanname, $tagcollid, $itemtype, $itemid); if ($component) { $query .= ' AND ti.component = ?'; $params[] = $component; } return $DB->record_exists_sql($query, $params) ? 1 : 0; } /** * Returns whether the tag area is enabled * * @param string $component component responsible for tagging * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. * @return bool|null */ public static function is_enabled($component, $itemtype) { return core_tag_area::is_enabled($component, $itemtype); } /** * Retrieves contents of tag area for the tag/index.php page * * @param stdClass $tagarea * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ public function get_tag_index($tagarea, $exclusivemode, $fromctx, $ctx, $rec, $page = 0) { global $CFG; if (!empty($tagarea->callback)) { if (!empty($tagarea->callbackfile)) { require_once($CFG->dirroot . '/' . ltrim($tagarea->callbackfile, '/')); } $callback = $tagarea->callback; return call_user_func_array($callback, [$this, $exclusivemode, $fromctx, $ctx, $rec, $page]); } return null; } /** * Returns formatted description of the tag * * @param array $options * @return string */ public function get_formatted_description($options = array()) { $options = empty($options) ? array() : (array)$options; $options += array('para' => false, 'overflowdiv' => true); $description = file_rewrite_pluginfile_urls($this->description, 'pluginfile.php', context_system::instance()->id, 'tag', 'description', $this->id); return format_text($description, $this->descriptionformat, $options); } /** * Returns the list of tag links available for the current user (edit, flag, etc.) * * @return array */ public function get_links() { global $USER; $links = array(); if (!isloggedin() || isguestuser()) { return $links; } $tagname = $this->get_display_name(); $systemcontext = context_system::instance(); // Add a link for users to add/remove this from their interests. if (static::is_enabled('core', 'user') && core_tag_area::get_collection('core', 'user') == $this->tagcollid) { if (static::is_item_tagged_with('core', 'user', $USER->id, $this->name)) { $url = new moodle_url('/tag/user.php', array('action' => 'removeinterest', 'sesskey' => sesskey(), 'tag' => $this->rawname)); $links[] = html_writer::link($url, get_string('removetagfrommyinterests', 'tag', $tagname), array('class' => 'removefrommyinterests')); } else { $url = new moodle_url('/tag/user.php', array('action' => 'addinterest', 'sesskey' => sesskey(), 'tag' => $this->rawname)); $links[] = html_writer::link($url, get_string('addtagtomyinterests', 'tag', $tagname), array('class' => 'addtomyinterests')); } } // Flag as inappropriate link. Only people with moodle/tag:flag capability. if (has_capability('moodle/tag:flag', $systemcontext)) { $url = new moodle_url('/tag/user.php', array('action' => 'flaginappropriate', 'sesskey' => sesskey(), 'id' => $this->id)); $links[] = html_writer::link($url, get_string('flagasinappropriate', 'tag', $tagname), array('class' => 'flagasinappropriate')); } // Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags. if (has_capability('moodle/tag:edit', $systemcontext) || has_capability('moodle/tag:manage', $systemcontext)) { $url = new moodle_url('/tag/edit.php', array('id' => $this->id)); $links[] = html_writer::link($url, get_string('edittag', 'tag'), array('class' => 'edittag')); } return $links; } /** * Delete one or more tag, and all their instances if there are any left. * * @param int|array $tagids one tagid (int), or one array of tagids to delete * @return bool true on success, false otherwise */ public static function delete_tags($tagids) { global $DB; if (!is_array($tagids)) { $tagids = array($tagids); } if (empty($tagids)) { return; } // Use the tagids to create a select statement to be used later. list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids); // Store the tags and tag instances we are going to delete. $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams); $taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams); // Delete all the tag instances. $select = 'WHERE tagid ' . $tagsql; $sql = "DELETE FROM {tag_instance} $select"; $DB->execute($sql, $tagparams); // Delete all the tag correlations. $sql = "DELETE FROM {tag_correlation} $select"; $DB->execute($sql, $tagparams); // Delete all the tags. $select = 'WHERE id ' . $tagsql; $sql = "DELETE FROM {tag} $select"; $DB->execute($sql, $tagparams); // Fire an event that these items were untagged. if ($taginstances) { // Save the system context in case the 'contextid' column in the 'tag_instance' table is null. $syscontextid = context_system::instance()->id; // Loop through the tag instances and fire a 'tag_removed'' event. foreach ($taginstances as $taginstance) { // We can not fire an event with 'null' as the contextid. if (is_null($taginstance->contextid)) { $taginstance->contextid = $syscontextid; } // Trigger tag removed event. \core\event\tag_removed::create_from_tag_instance($taginstance, $tags[$taginstance->tagid]->name, $tags[$taginstance->tagid]->rawname, true)->trigger(); } } // Fire an event that these tags were deleted. if ($tags) { $context = context_system::instance(); foreach ($tags as $tag) { // Delete all files associated with this tag. $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id); foreach ($files as $file) { $file->delete(); } // Trigger an event for deleting this tag. $event = \core\event\tag_deleted::create(array( 'objectid' => $tag->id, 'relateduserid' => $tag->userid, 'context' => $context, 'other' => array( 'name' => $tag->name, 'rawname' => $tag->rawname ) )); $event->add_record_snapshot('tag', $tag); $event->trigger(); } } return true; } /** * Combine together correlated tags of several tags * * This is a help method for method combine_tags() * * @param core_tag_tag[] $tags */ protected function combine_correlated_tags($tags) { global $DB; $ids = array_map(function($t) { return $t->id; }, $tags); // Retrieve the correlated tags of this tag and correlated tags of all tags to be merged in one query // but store them separately. Calculate the list of correlated tags that need to be added to the current. list($sql, $params) = $DB->get_in_or_equal($ids); $params[] = $this->id; $records = $DB->get_records_select('tag_correlation', 'tagid '.$sql.' OR tagid = ?', $params, '', 'tagid, id, correlatedtags'); $correlated = array(); $mycorrelated = array(); foreach ($records as $record) { $taglist = preg_split('/\s*,\s*/', trim($record->correlatedtags), -1, PREG_SPLIT_NO_EMPTY); if ($record->tagid == $this->id) { $mycorrelated = $taglist; } else { $correlated = array_merge($correlated, $taglist); } } array_unique($correlated); // Strip out from $correlated the ids of the tags that are already in $mycorrelated // or are one of the tags that are going to be combined. $correlated = array_diff($correlated, [$this->id], $ids, $mycorrelated); if (empty($correlated)) { // Nothing to do, ignore situation when current tag is correlated to one of the merged tags - they will // be deleted later and get_tag_correlation() will not return them. Next cron will clean everything up. return; } // Update correlated tags of this tag. $newcorrelatedlist = join(',', array_merge($mycorrelated, $correlated)); if (isset($records[$this->id])) { $DB->update_record('tag_correlation', array('id' => $records[$this->id]->id, 'correlatedtags' => $newcorrelatedlist)); } else { $DB->insert_record('tag_correlation', array('tagid' => $this->id, 'correlatedtags' => $newcorrelatedlist)); } // Add this tag to the list of correlated tags of each tag in $correlated. list($sql, $params) = $DB->get_in_or_equal($correlated); $records = $DB->get_records_select('tag_correlation', 'tagid '.$sql, $params, '', 'tagid, id, correlatedtags'); foreach ($correlated as $tagid) { if (isset($records[$tagid])) { $newcorrelatedlist = $records[$tagid]->correlatedtags . ',' . $this->id; $DB->update_record('tag_correlation', array('id' => $records[$tagid]->id, 'correlatedtags' => $newcorrelatedlist)); } else { $DB->insert_record('tag_correlation', array('tagid' => $tagid, 'correlatedtags' => '' . $this->id)); } } } /** * Combines several other tags into this one * * Combining rules: * - current tag becomes the "main" one, all instances * pointing to other tags are changed to point to it. * - if any of the tags is standard, the "main" tag becomes standard too * - all tags except for the current ("main") are deleted, even when they are standard * * @param core_tag_tag[] $tags tags to combine into this one */ public function combine_tags($tags) { global $DB; $this->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'name', 'rawname'), 'combine_tags'); // Retrieve all tag objects, find if there are any standard tags in the set. $isstandard = false; $tagstocombine = array(); $ids = array(); $relatedtags = $this->get_manual_related_tags(); foreach ($tags as $tag) { $tag->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'tagcollid', 'name', 'rawname'), 'combine_tags'); if ($tag && $tag->id != $this->id && $tag->tagcollid == $this->tagcollid) { $isstandard = $isstandard || $tag->isstandard; $tagstocombine[$tag->name] = $tag; $ids[] = $tag->id; $relatedtags = array_merge($relatedtags, $tag->get_manual_related_tags()); } } if (empty($tagstocombine)) { // Nothing to do. return; } // Combine all manually set related tags, exclude itself all the tags it is about to be combined with. if ($relatedtags) { $relatedtags = array_map(function($t) { return $t->name; }, $relatedtags); array_unique($relatedtags); $relatedtags = array_diff($relatedtags, [$this->name], array_keys($tagstocombine)); } $this->set_related_tags($relatedtags); // Combine all correlated tags, exclude itself all the tags it is about to be combined with. $this->combine_correlated_tags($tagstocombine); // If any of the duplicate tags are standard, mark this one as standard too. if ($isstandard && !$this->isstandard) { $this->update(array('isstandard' => 1)); } // Go through all instances of each tag that needs to be combined and make them point to this tag instead. // We go though the list one by one because otherwise looking-for-duplicates logic would be too complicated. foreach ($tagstocombine as $tag) { $params = array('tagid' => $tag->id, 'mainid' => $this->id); $mainsql = 'SELECT ti.*, t.name, t.rawname, tim.id AS alreadyhasmaintag ' . 'FROM {tag_instance} ti ' . 'LEFT JOIN {tag} t ON t.id = ti.tagid ' . 'LEFT JOIN {tag_instance} tim ON ti.component = tim.component AND ' . ' ti.itemtype = tim.itemtype AND ti.itemid = tim.itemid AND ' . ' ti.tiuserid = tim.tiuserid AND tim.tagid = :mainid ' . 'WHERE ti.tagid = :tagid'; $records = $DB->get_records_sql($mainsql, $params); foreach ($records as $record) { if ($record->alreadyhasmaintag) { // Item is tagged with both main tag and the duplicate tag. // Remove instance pointing to the duplicate tag. $tag->delete_instance_as_record($record, false); $sql = "UPDATE {tag_instance} SET ordering = ordering - 1 WHERE itemtype = :itemtype AND itemid = :itemid AND component = :component AND tiuserid = :tiuserid AND ordering > :ordering"; $DB->execute($sql, (array)$record); } else { // Item is tagged only with duplicate tag but not the main tag. // Replace tagid in the instance pointing to the duplicate tag with this tag. $DB->update_record('tag_instance', array('id' => $record->id, 'tagid' => $this->id)); \core\event\tag_removed::create_from_tag_instance($record, $record->name, $record->rawname)->trigger(); $record->tagid = $this->id; \core\event\tag_added::create_from_tag_instance($record, $this->name, $this->rawname)->trigger(); } } } // Finally delete all tags that we combined into the current one. self::delete_tags($ids); } /** * Retrieve a list of tags that have been used to tag the given $component * and $itemtype in the provided $contexts. * * @param string $component The tag instance component * @param string $itemtype The tag instance item type * @param context[] $contexts The list of contexts to look for tag instances in * @return core_tag_tag[] */ public static function get_tags_by_area_in_contexts($component, $itemtype, array $contexts) { global $DB; $params = [$component, $itemtype]; $contextids = array_map(function($context) { return $context->id; }, $contexts); list($contextsql, $contextsqlparams) = $DB->get_in_or_equal($contextids); $params = array_merge($params, $contextsqlparams); $subsql = "SELECT DISTINCT t.id FROM {tag} t JOIN {tag_instance} ti ON t.id = ti.tagid WHERE component = ? AND itemtype = ? AND contextid {$contextsql}"; $sql = "SELECT tt.* FROM ($subsql) tv JOIN {tag} tt ON tt.id = tv.id"; return array_map(function($record) { return new core_tag_tag($record); }, $DB->get_records_sql($sql, $params)); } } external.php 0000644 00000016536 15151222237 0007112 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Resource external API * * @package mod_resource * @category external * @copyright 2015 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ use core_course\external\helper_for_get_mods_by_courses; defined('MOODLE_INTERNAL') || die; require_once("$CFG->libdir/externallib.php"); /** * Resource external functions * * @package mod_resource * @category external * @copyright 2015 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ class mod_resource_external extends external_api { /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function view_resource_parameters() { return new external_function_parameters( array( 'resourceid' => new external_value(PARAM_INT, 'resource instance id') ) ); } /** * Simulate the resource/view.php web interface page: trigger events, completion, etc... * * @param int $resourceid the resource instance id * @return array of warnings and status result * @since Moodle 3.0 * @throws moodle_exception */ public static function view_resource($resourceid) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/resource/lib.php"); $params = self::validate_parameters(self::view_resource_parameters(), array( 'resourceid' => $resourceid )); $warnings = array(); // Request and permission validation. $resource = $DB->get_record('resource', array('id' => $params['resourceid']), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($resource, 'resource'); $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/resource:view', $context); // Call the resource/lib API. resource_view($resource, $course, $cm, $context); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return external_description * @since Moodle 3.0 */ public static function view_resource_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Describes the parameters for get_resources_by_courses. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_resources_by_courses_parameters() { return new external_function_parameters ( array( 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, array() ), ) ); } /** * Returns a list of files in a provided list of courses. * If no list is provided all files that the user can view will be returned. * * @param array $courseids course ids * @return array of warnings and files * @since Moodle 3.3 */ public static function get_resources_by_courses($courseids = array()) { $warnings = array(); $returnedresources = array(); $params = array( 'courseids' => $courseids, ); $params = self::validate_parameters(self::get_resources_by_courses_parameters(), $params); $mycourses = array(); if (empty($params['courseids'])) { $mycourses = enrol_get_my_courses(); $params['courseids'] = array_keys($mycourses); } // Ensure there are courseids to loop through. if (!empty($params['courseids'])) { list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses); // Get the resources in this course, this function checks users visibility permissions. // We can avoid then additional validate_context calls. $resources = get_all_instances_in_courses("resource", $courses); foreach ($resources as $resource) { $context = context_module::instance($resource->coursemodule); helper_for_get_mods_by_courses::format_name_and_intro($resource, 'mod_resource'); $resource->contentfiles = external_util::get_area_files($context->id, 'mod_resource', 'content'); $returnedresources[] = $resource; } } $result = array( 'resources' => $returnedresources, 'warnings' => $warnings ); return $result; } /** * Describes the get_resources_by_courses return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_resources_by_courses_returns() { return new external_single_structure( array( 'resources' => new external_multiple_structure( new external_single_structure(array_merge( helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(), [ 'contentfiles' => new external_files('Files in the content'), 'tobemigrated' => new external_value(PARAM_INT, 'Whether this resource was migrated'), 'legacyfiles' => new external_value(PARAM_INT, 'Legacy files flag'), 'legacyfileslast' => new external_value(PARAM_INT, 'Legacy files last control flag'), 'display' => new external_value(PARAM_INT, 'How to display the resource'), 'displayoptions' => new external_value(PARAM_RAW, 'Display options (width, height)'), 'filterfiles' => new external_value(PARAM_INT, 'If filters should be applied to the resource content'), 'revision' => new external_value(PARAM_INT, 'Incremented when after each file changes, to avoid cache'), 'timemodified' => new external_value(PARAM_INT, 'Last time the resource was modified'), ] )) ), 'warnings' => new external_warnings(), ) ); } } index_builder.php 0000644 00000034151 15151222237 0010076 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class core_tag_index_builder * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper to build tag index * * This can be used by components to implement tag area callbacks. This is especially * useful for in-course content when we need to check and cache user's access to * multiple courses. Course access and accessible items are stored in session cache * with 15 minutes expiry time. * * Example of usage: * * $builder = new core_tag_index_builder($component, $itemtype, $sql, $params, $from, $limit); * while ($item = $builder->has_item_that_needs_access_check()) { * if (!$builder->can_access_course($item->courseid)) { * $builder->set_accessible($item, false); * } else { * $accessible = true; // Check access and set $accessible respectively. * $builder->set_accessible($item, $accessible); * } * } * $items = $builder->get_items(); * * @package core_tag * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_index_builder { /** @var string component specified in the constructor */ protected $component; /** @var string itemtype specified in the constructor */ protected $itemtype; /** @var string SQL statement */ protected $sql; /** @var array parameters for SQL statement */ protected $params; /** @var int index from which to return records */ protected $from; /** @var int maximum number of records to return */ protected $limit; /** @var array result of SQL query */ protected $items; /** @var array list of item ids ( array_keys($this->items) ) */ protected $itemkeys; /** @var string alias of the item id in the SQL result */ protected $idfield = 'id'; /** @var array cache of items accessibility (id => bool) */ protected $accessibleitems; /** @var array cache of courses accessibility (courseid => bool) */ protected $courseaccess; /** @var bool indicates that items cache was changed in this class and needs pushing to MUC */ protected $cachechangedaccessible = false; /** @var bool indicates that course accessibiity cache was changed in this class and needs pushing to MUC */ protected $cachechangedcourse = false; /** @var array cached courses (not pushed to MUC) */ protected $courses; /** * Constructor. * * Specify the SQL query for retrieving the tagged items, SQL query must: * - return the item id as the first field and make sure that it is unique in the result * - provide ORDER BY that exclude any possibility of random results, if $fromctx was specified when searching * for tagged items it is the best practice to make sure that items from this context are returned first. * * This query may also contain placeholders %COURSEFILTER% or %ITEMFILTER% that will be substituted with * expressions excluding courses and/or filters that are already known as inaccessible. * * Example: "WHERE c.id %COURSEFILTER% AND cm.id %ITEMFILTER%" * * This query may contain fields to preload context if context is needed for formatting values. * * It is recommended to sort by course sortorder first, this way the items from the same course will be next to * each other and the sequence of courses will the same in different tag areas. * * @param string $component component responsible for tagging * @param string $itemtype type of item that is being tagged * @param string $sql SQL query that would retrieve all relevant items without permission check * @param array $params parameters for the query (must be named) * @param int $from return a subset of records, starting at this point * @param int $limit return a subset comprising this many records in total (this field is NOT optional) */ public function __construct($component, $itemtype, $sql, $params, $from, $limit) { $this->component = preg_replace('/[^A-Za-z0-9_]/i', '', $component); $this->itemtype = preg_replace('/[^A-Za-z0-9_]/i', '', $itemtype); $this->sql = $sql; $this->params = $params; $this->from = $from; $this->limit = $limit; $this->courses = array(); } /** * Substitute %COURSEFILTER% with an expression filtering out courses where current user does not have access */ protected function prepare_sql_courses() { global $DB; if (!preg_match('/\\%COURSEFILTER\\%/', $this->sql)) { return; } $this->init_course_access(); $unaccessiblecourses = array_filter($this->courseaccess, function($item) { return !$item; }); $idx = 0; while (preg_match('/^([^\\0]*?)\\%COURSEFILTER\\%([^\\0]*)$/', $this->sql, $matches)) { list($sql, $params) = $DB->get_in_or_equal(array_keys($unaccessiblecourses), SQL_PARAMS_NAMED, 'ca_'.($idx++).'_', false, 0); $this->sql = $matches[1].' '.$sql.' '.$matches[2]; $this->params += $params; } } /** * Substitute %ITEMFILTER% with an expression filtering out items where current user does not have access */ protected function prepare_sql_items() { global $DB; if (!preg_match('/\\%ITEMFILTER\\%/', $this->sql)) { return; } $this->init_items_access(); $unaccessibleitems = array_filter($this->accessibleitems, function($item) { return !$item; }); $idx = 0; while (preg_match('/^([^\\0]*?)\\%ITEMFILTER\\%([^\\0]*)$/', $this->sql, $matches)) { list($sql, $params) = $DB->get_in_or_equal(array_keys($unaccessibleitems), SQL_PARAMS_NAMED, 'ia_'.($idx++).'_', false, 0); $this->sql = $matches[1].' '.$sql.' '.$matches[2]; $this->params += $params; } } /** * Ensures that SQL query was executed and $this->items is filled */ protected function retrieve_items() { global $DB; if ($this->items !== null) { return; } $this->prepare_sql_courses(); $this->prepare_sql_items(); $this->items = $DB->get_records_sql($this->sql, $this->params); $this->itemkeys = array_keys($this->items); if ($this->items) { // Find the name of the first key of the item - usually 'id' but can be something different. // This must be a unique identifier of the item. $firstitem = reset($this->items); $firstitemarray = (array)$firstitem; $this->idfield = key($firstitemarray); } } /** * Returns the filtered records from SQL query result. * * This function can only be executed after $builder->has_item_that_needs_access_check() returns null * * * @return array */ public function get_items() { global $DB, $CFG; if (is_siteadmin()) { $this->sql = preg_replace('/\\%COURSEFILTER\\%/', '<>0', $this->sql); $this->sql = preg_replace('/\\%ITEMFILTER\\%/', '<>0', $this->sql); return $DB->get_records_sql($this->sql, $this->params, $this->from, $this->limit); } if ($CFG->debugdeveloper && $this->has_item_that_needs_access_check()) { debugging('Caller must ensure that has_item_that_needs_access_check() does not return anything ' . 'before calling get_items(). The item list may be incomplete', DEBUG_DEVELOPER); } $this->retrieve_items(); $this->save_caches(); $idx = 0; $items = array(); foreach ($this->itemkeys as $id) { if (!array_key_exists($id, $this->accessibleitems) || !$this->accessibleitems[$id]) { continue; } if ($idx >= $this->from) { $items[$id] = $this->items[$id]; } $idx++; if ($idx >= $this->from + $this->limit) { break; } } return $items; } /** * Returns the first row from the SQL result that we don't know whether it is accessible by user or not. * * This will return null when we have necessary number of accessible items to return in {@link get_items()} * * After analyzing you may decide to mark not only this record but all similar as accessible or not accessible. * For example, if you already call get_fast_modinfo() to check this item's accessibility, why not mark all * items in the same course as accessible or not accessible. * * Helpful methods: {@link set_accessible()} and {@link walk()} * * @return null|object */ public function has_item_that_needs_access_check() { if (is_siteadmin()) { return null; } $this->retrieve_items(); $counter = 0; // Counter for accessible items. foreach ($this->itemkeys as $id) { if (!array_key_exists($id, $this->accessibleitems)) { return (object)(array)$this->items[$id]; } $counter += $this->accessibleitems[$id] ? 1 : 0; if ($counter >= $this->from + $this->limit) { // We found enough accessible items fot get_items() method, do not look any further. return null; } } return null; } /** * Walk through the array of items and call $callable for each of them * @param callable $callable */ public function walk($callable) { $this->retrieve_items(); array_walk($this->items, $callable); } /** * Marks record or group of records as accessible (or not accessible) * * @param int|std_Class $identifier either record id of the item that needs to be set accessible * @param bool $accessible whether to mark as accessible or not accessible (default true) */ public function set_accessible($identifier, $accessible = true) { if (is_object($identifier)) { $identifier = (int)($identifier->{$this->idfield}); } $this->init_items_access(); if (is_int($identifier)) { $accessible = (int)(bool)$accessible; if (!array_key_exists($identifier, $this->accessibleitems) || $this->accessibleitems[$identifier] != $accessible) { $this->accessibleitems[$identifier] = $accessible; $this->cachechangedaccessible; } } else { throw new coding_exception('Argument $identifier must be either int or object'); } } /** * Retrieves a course record (only fields id,visible,fullname,shortname,cacherev). * * This method is useful because it also caches results and preloads course context. * * @param int $courseid */ public function get_course($courseid) { global $DB; if (!array_key_exists($courseid, $this->courses)) { $ctxquery = context_helper::get_preload_record_columns_sql('ctx'); $sql = "SELECT c.id,c.visible,c.fullname,c.shortname,c.cacherev, $ctxquery FROM {course} c JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid=c.id WHERE c.id = ?"; $params = array(CONTEXT_COURSE, $courseid); $this->courses[$courseid] = $DB->get_record_sql($sql, $params); context_helper::preload_from_record($this->courses[$courseid]); } return $this->courses[$courseid]; } /** * Ensures that we read the course access from the cache. */ protected function init_course_access() { if ($this->courseaccess === null) { $this->courseaccess = cache::make('core', 'tagindexbuilder')->get('courseaccess') ?: []; } } /** * Ensures that we read the items access from the cache. */ protected function init_items_access() { if ($this->accessibleitems === null) { $this->accessibleitems = cache::make('core', 'tagindexbuilder')->get($this->component.'__'.$this->itemtype) ?: []; } } /** * Checks if current user has access to the course * * This method calls global function {@link can_access_course} and caches results * * @param int $courseid * @return bool */ public function can_access_course($courseid) { $this->init_course_access(); if (!array_key_exists($courseid, $this->courseaccess)) { $this->courseaccess[$courseid] = can_access_course($this->get_course($courseid)) ? 1 : 0; $this->cachechangedcourse = true; } return $this->courseaccess[$courseid]; } /** * Saves course/items caches if needed */ protected function save_caches() { if ($this->cachechangedcourse) { cache::make('core', 'tagindexbuilder')->set('courseaccess', $this->courseaccess); $this->cachechangedcourse = false; } if ($this->cachechangedaccessible) { cache::make('core', 'tagindexbuilder')->set($this->component.'__'.$this->itemtype, $this->accessibleitems); $this->cachechangedaccessible = false; } } /** * Resets all course/items session caches - useful in unittests when we change users and enrolments. */ public static function reset_caches() { cache_helper::purge_by_event('resettagindexbuilder'); } } reportbuilder/datasource/tags.php 0000644 00000007154 15151222237 0013236 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_tag\reportbuilder\datasource; use lang_string; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\user; use core_tag\reportbuilder\local\entities\{collection, tag, instance}; /** * Tags datasource * * @package core_tag * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tags extends datasource { /** * Return user friendly name of the report source * * @return string */ public static function get_name(): string { return get_string('tags', 'core_tag'); } /** * Initialise report */ protected function initialise(): void { $collectionentity = new collection(); $collectionalias = $collectionentity->get_table_alias('tag_coll'); $this->set_main_table('tag_coll', $collectionalias); $this->add_entity($collectionentity); // Join tag entity to collection. $tagentity = new tag(); $tagalias = $tagentity->get_table_alias('tag'); $this->add_entity($tagentity ->add_join("LEFT JOIN {tag} {$tagalias} ON {$tagalias}.tagcollid = {$collectionalias}.id") ); // Join instance entity to tag. $instanceentity = new instance(); $instancealias = $instanceentity->get_table_alias('tag_instance'); $this->add_entity($instanceentity ->add_joins($tagentity->get_joins()) ->add_join("LEFT JOIN {tag_instance} {$instancealias} ON {$instancealias}.tagid = {$tagalias}.id") ); // Join user entity to represent the tag author. $userentity = (new user()) ->set_entity_title(new lang_string('tagauthor', 'core_tag')); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($tagentity->get_joins()) ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$tagalias}.userid") ); // Add report elements from each of the entities we added to the report. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report upon creation * * @return string[] */ public function get_default_columns(): array { return [ 'collection:name', 'tag:namewithlink', 'tag:standard', 'instance:context', ]; } /** * Return the filters that will be added to the report upon creation * * @return string[] */ public function get_default_filters(): array { return [ 'tag:name', 'tag:standard', ]; } /** * Return the conditions that will be added to the report upon creation * * @return string[] */ public function get_default_conditions(): array { return [ 'collection:name', ]; } } reportbuilder/local/entities/collection.php 0000644 00000014066 15151222237 0015217 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_tag\reportbuilder\local\entities; use core_tag_collection; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{boolean_select, select}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Tag collection entity * * @package core_tag * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class collection extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return ['tag_coll' => 'tc']; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('tagcollection', 'core_tag'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $collectionalias = $this->get_table_alias('tag_coll'); // Name. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$collectionalias}.name, {$collectionalias}.component, {$collectionalias}.isdefault, {$collectionalias}.id") ->set_is_sortable(true) ->add_callback(static function(?string $name, stdClass $collection): string { return core_tag_collection::display_name($collection); }); // Default. $columns[] = (new column( 'default', new lang_string('defautltagcoll', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$collectionalias}.isdefault") ->set_is_sortable(true) ->add_callback([format::class, 'boolean_as_text']); // Component. $columns[] = (new column( 'component', new lang_string('component', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$collectionalias}.component") ->set_is_sortable(true); // Searchable. $columns[] = (new column( 'searchable', new lang_string('searchable', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$collectionalias}.searchable") ->set_is_sortable(true) ->add_callback([format::class, 'boolean_as_text']); // Custom URL. $columns[] = (new column( 'customurl', new lang_string('url'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$collectionalias}.customurl") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $collectionalias = $this->get_table_alias('tag_coll'); // Name. $filters[] = (new filter( select::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$collectionalias}.id" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { global $DB; $collections = $DB->get_records('tag_coll', [], 'sortorder', 'id, name, component, isdefault'); return array_map(static function(stdClass $collection): string { return core_tag_collection::display_name($collection); }, $collections); }); // Default. $filters[] = (new filter( boolean_select::class, 'default', new lang_string('defautltagcoll', 'core_tag'), $this->get_entity_name(), "{$collectionalias}.isdefault" )) ->add_joins($this->get_joins()); // Searchable. $filters[] = (new filter( boolean_select::class, 'searchable', new lang_string('searchable', 'core_tag'), $this->get_entity_name(), "{$collectionalias}.searchable" )) ->add_joins($this->get_joins()); return $filters; } } reportbuilder/local/entities/tag.php 0000644 00000017402 15151222237 0013634 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_tag\reportbuilder\local\entities; use context_system; use core_tag_tag; use html_writer; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{boolean_select, date, tags}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Tag entity * * @package core_tag * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tag extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return ['tag' => 't']; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('tag', 'core_tag'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $tagalias = $this->get_table_alias('tag'); // Name. $columns[] = (new column( 'name', new lang_string('name', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tagalias}.rawname, {$tagalias}.name") ->set_is_sortable(true) ->add_callback(static function($rawname, stdClass $tag): string { if ($rawname === null) { return ''; } return core_tag_tag::make_display_name($tag); }); // Name with link. $columns[] = (new column( 'namewithlink', new lang_string('namewithlink', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tagalias}.rawname, {$tagalias}.name, {$tagalias}.tagcollid") ->set_is_sortable(true) ->add_callback(static function($rawname, stdClass $tag): string { if ($rawname === null) { return ''; } return html_writer::link(core_tag_tag::make_url($tag->tagcollid, $tag->rawname), core_tag_tag::make_display_name($tag)); }); // Description. $descriptionfieldsql = "{$tagalias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('tagdescription', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$tagalias}.descriptionformat, {$tagalias}.id") ->add_callback(static function(?string $description, stdClass $tag): string { global $CFG; require_once("{$CFG->libdir}/filelib.php"); if ($description === null) { return ''; } $context = context_system::instance(); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'tag', 'description', $tag->id); return format_text($description, $tag->descriptionformat, ['context' => $context->id]); }); // Standard. $columns[] = (new column( 'standard', new lang_string('standardtag', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$tagalias}.isstandard") ->set_is_sortable(true) ->add_callback([format::class, 'boolean_as_text']); // Flagged. $columns[] = (new column( 'flagged', new lang_string('flagged', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$tagalias}.flag") ->set_is_sortable(true) ->add_callback([format::class, 'boolean_as_text']); // Time modified. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tagalias}.timemodified") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tagalias = $this->get_table_alias('tag'); // Name. $filters[] = (new filter( tags::class, 'name', new lang_string('name', 'core_tag'), $this->get_entity_name(), "{$tagalias}.id" )) ->add_joins($this->get_joins()); // Standard. $filters[] = (new filter( boolean_select::class, 'standard', new lang_string('standardtag', 'core_tag'), $this->get_entity_name(), "{$tagalias}.isstandard" )) ->add_joins($this->get_joins()); // Flagged. $filters[] = (new filter( boolean_select::class, 'flagged', new lang_string('flagged', 'core_tag'), $this->get_entity_name(), "{$tagalias}.flag" )) ->add_joins($this->get_joins()); // Time modified. $filters[] = (new filter( date::class, 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name(), "{$tagalias}.timemodified" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_CURRENT, date::DATE_LAST, date::DATE_RANGE, ]); return $filters; } } reportbuilder/local/entities/instance.php 0000644 00000022433 15151222237 0014665 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_tag\reportbuilder\local\entities; use context; use context_helper; use core_collator; use core_tag_area; use html_writer; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, select}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Tag instance entity * * @package core_tag * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class instance extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'tag_instance' => 'ti', 'context' => 'tictx', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('taginstance', 'core_tag'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $instancealias = $this->get_table_alias('tag_instance'); $contextalias = $this->get_table_alias('context'); // Area. $columns[] = (new column( 'area', new lang_string('tagarea', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$instancealias}.component, {$instancealias}.itemtype") ->set_is_sortable(true, ["{$instancealias}.component", "{$instancealias}.itemtype"]) ->add_callback(static function($component, stdClass $area): string { if ($component === null) { return ''; } return (string) core_tag_area::display_name($area->component, $area->itemtype); }); // Context. $columns[] = (new column( 'context', new lang_string('context'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$instancealias}.contextid") ->add_fields("{$instancealias}.contextid, " . context_helper::get_preload_record_columns_sql($contextalias)) // Sorting may not order alphabetically, but will at least group contexts together. ->set_is_sortable(true) ->add_callback(static function($contextid, stdClass $context): string { if ($contextid === null) { return ''; } context_helper::preload_from_record($context); return context::instance_by_id($contextid)->get_context_name(); }); // Context URL. $columns[] = (new column( 'contexturl', new lang_string('contexturl'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$instancealias}.contextid") ->add_fields("{$instancealias}.contextid, " . context_helper::get_preload_record_columns_sql($contextalias)) // Sorting may not order alphabetically, but will at least group contexts together. ->set_is_sortable(true) ->add_callback(static function($contextid, stdClass $context): string { if ($contextid === null) { return ''; } context_helper::preload_from_record($context); $context = context::instance_by_id($contextid); return html_writer::link($context->get_url(), $context->get_context_name()); }); // Component. $columns[] = (new column( 'component', new lang_string('component', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$instancealias}.component") ->set_is_sortable(true); // Item type. $columns[] = (new column( 'itemtype', new lang_string('itemtype', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$instancealias}.itemtype") ->set_is_sortable(true); // Item ID. $columns[] = (new column( 'itemid', new lang_string('itemid', 'core_tag'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$instancealias}.itemid") ->set_is_sortable(true) ->set_disabled_aggregation_all(); // Time created. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$instancealias}.timecreated") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time modified. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$instancealias}.timemodified") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { global $DB; $instancealias = $this->get_table_alias('tag_instance'); // Area. $filters[] = (new filter( select::class, 'area', new lang_string('tagarea', 'core_tag'), $this->get_entity_name(), $DB->sql_concat("{$instancealias}.component", "'/'", "{$instancealias}.itemtype") )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { $options = []; foreach (core_tag_area::get_areas() as $areas) { foreach ($areas as $area) { $options["{$area->component}/{$area->itemtype}"] = core_tag_area::display_name( $area->component, $area->itemtype); } } core_collator::asort($options); return $options; }); // Time created. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$instancealias}.timecreated" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_CURRENT, date::DATE_LAST, date::DATE_RANGE, ]); // Time modified. $filters[] = (new filter( date::class, 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name(), "{$instancealias}.timemodified" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_CURRENT, date::DATE_LAST, date::DATE_RANGE, ]); return $filters; } } collections_table.php 0000644 00000010456 15151222237 0010750 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_tag_collections_table * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Table with the list of tag collections for "Manage tags" page. * * @package core_tag * @copyright 2015 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_tag_collections_table extends html_table { /** * Constructor * @param string|moodle_url $pageurl */ public function __construct($pageurl) { global $OUTPUT; parent::__construct(); $this->attributes['class'] = 'generaltable tag-collections-table'; $this->head = array( get_string('name'), get_string('component', 'tag'), get_string('tagareas', 'tag'), get_string('searchable', 'tag') . $OUTPUT->help_icon('searchable', 'tag'), '' ); $this->data = array(); $tagcolls = core_tag_collection::get_collections(); $idx = 0; foreach ($tagcolls as $tagcoll) { $actions = ''; $name = core_tag_collection::display_name($tagcoll); $url = new moodle_url($pageurl, array('sesskey' => sesskey(), 'tc' => $tagcoll->id)); if (!$tagcoll->isdefault) { // Move up. if ($idx > 1) { $url->param('action', 'collmoveup'); $actions .= $OUTPUT->action_icon($url, new pix_icon('t/up', get_string('moveup')), null, array('class' => 'action-icon action_moveup')); } // Move down. if ($idx < count($tagcolls) - 1) { $url->param('action', 'collmovedown'); $actions .= $OUTPUT->action_icon($url, new pix_icon('t/down', get_string('movedown')), null, array('class' => 'action-icon action_movedown')); } } if (!$tagcoll->isdefault && empty($tagcoll->component)) { // Delete. $url->param('action', 'colldelete'); $actions .= $OUTPUT->action_icon('#', new pix_icon('t/delete', get_string('delete')), null, array('data-url' => $url->out(false), 'data-collname' => $name, 'class' => 'action-icon action_delete')); } $component = ''; if ($tagcoll->component) { $component = ($tagcoll->component === 'core' || preg_match('/^core_/', $tagcoll->component)) ? get_string('coresystem') : get_string('pluginname', $tagcoll->component); } $allareas = core_tag_collection::get_areas_names(null, false); $validareas = core_tag_collection::get_areas_names($tagcoll->id); $areaslist = array_map(function($key) use ($allareas, $validareas) { return "<li data-areaid=\"{$key}\" " . (array_key_exists($key, $validareas) ? "" : "style=\"display:none;\"") . ">{$allareas[$key]}</li>"; }, array_keys($allareas)); $displayname = new \core_tag\output\tagcollname($tagcoll); $searchable = new \core_tag\output\tagcollsearchable($tagcoll); $this->data[] = array( $displayname->render($OUTPUT), $component, "<ul data-collectionid=\"{$tagcoll->id}\">" . join('', $areaslist) . '</ul>', $searchable->render($OUTPUT), $actions); $idx++; } } } external/tag_item_exporter.php 0000644 00000005645 15151222237 0012632 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains related class for displaying information of a tag item. * * @package core_tag * @copyright 2019 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Contains related class for displaying information of a tag item. * * @package core_tag * @copyright 2019 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tag_item_exporter extends exporter { /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'id' => [ 'type' => PARAM_INT, 'description' => 'Tag id.', ], 'name' => [ 'type' => PARAM_TAG, 'description' => 'Tag name.', ], 'rawname' => [ 'type' => PARAM_RAW, 'description' => 'The raw, unnormalised name for the tag as entered by users.', ], 'isstandard' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this tag is standard.', 'default' => false, ], 'tagcollid' => [ 'type' => PARAM_INT, 'description' => 'Tag collection id.', ], 'taginstanceid' => [ 'type' => PARAM_INT, 'description' => 'Tag instance id.', ], 'taginstancecontextid' => [ 'type' => PARAM_INT, 'description' => 'Context the tag instance belongs to.', ], 'itemid' => [ 'type' => PARAM_INT, 'description' => 'Id of the record tagged.', ], 'ordering' => [ 'type' => PARAM_INT, 'description' => 'Tag ordering.', ], 'flag' => [ 'type' => PARAM_INT, 'description' => 'Whether the tag is flagged as inappropriate.', 'default' => 0, 'null' => NULL_ALLOWED, ], ]; } } external/util.php 0000644 00000004640 15151222237 0010060 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Tag external functions utility class. * * @package core_tag * @copyright 2019 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\external; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/externallib.php'); use core_tag\external\tag_item_exporter; use core_tag_tag; /** * Tag external functions utility class. * * @package core_tag * @copyright 2019 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.7 */ class util { /** * Get the array of core_tag_tag objects for external functions associated with an item (instances). * * @param string $component component responsible for tagging. For BC it can be empty but in this case the * query will be slow because DB index will not be used. * @param string $itemtype type of the tagged item * @param int $itemid * @param int $standardonly wether to return only standard tags or any * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging * @return array tags for external functions */ public static function get_item_tags($component, $itemtype, $itemid, $standardonly = core_tag_tag::BOTH_STANDARD_AND_NOT, $tiuserid = 0) { global $PAGE; $output = $PAGE->get_renderer('core'); $tagitems = core_tag_tag::get_item_tags($component, $itemtype, $itemid, $standardonly, $tiuserid); $exportedtags = []; foreach ($tagitems as $tagitem) { $exporter = new tag_item_exporter($tagitem->to_object()); $exportedtags[] = (array) $exporter->export($output); } return $exportedtags; } } external/tag_area_exporter.php 0000644 00000007206 15151222237 0012577 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains related class for displaying information of a tag area. * * @package core_tag * @copyright 2019 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; use renderer_base; /** * Contains related class for displaying information of a tag area. * * @package core_tag * @copyright 2019 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tag_area_exporter extends exporter { /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'id' => [ 'type' => PARAM_INT, 'description' => 'Area id.', ], 'component' => [ 'type' => PARAM_COMPONENT, 'description' => 'Component the area is related to.', ], 'itemtype' => [ 'type' => PARAM_ALPHANUMEXT, 'description' => 'Type of item in the component.', ], 'enabled' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this area is enabled.', 'default' => true, ], 'tagcollid' => [ 'type' => PARAM_INT, 'description' => 'The tag collection this are belongs to.', ], 'callback' => [ 'type' => PARAM_RAW, 'description' => 'Component callback for processing tags.', 'null' => NULL_ALLOWED, ], 'callbackfile' => [ 'type' => PARAM_RAW, 'description' => 'Component callback file.', 'null' => NULL_ALLOWED, ], 'showstandard' => [ 'type' => PARAM_INT, 'description' => 'Return whether to display only standard, only non-standard or both tags.', 'default' => 0, ], 'multiplecontexts' => [ 'type' => PARAM_BOOL, 'description' => 'Whether the tag area allows tag instances to be created in multiple contexts. ', 'default' => false, ], ]; } protected static function define_related() { return array( 'locked' => 'bool?' ); } protected static function define_other_properties() { return array( 'locked' => [ 'type' => PARAM_BOOL, 'description' => 'Whether the area is locked.', 'null' => NULL_ALLOWED, 'default' => false, 'optional' => true, ] ); } protected function get_other_values(renderer_base $output) { $values['locked'] = $this->related['locked'] ? true : false; return $values; } } external/tag_collection_exporter.php 0000644 00000005230 15151222237 0014015 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains related class for displaying information of a tag collection. * * @package core_tag * @copyright 2019 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_tag\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Contains related class for displaying information of a tag collection. * * @package core_tag * @copyright 2019 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tag_collection_exporter extends exporter { /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'id' => [ 'type' => PARAM_INT, 'description' => 'Collection id.', ], 'name' => [ 'type' => PARAM_NOTAGS, 'description' => 'Collection name.', 'null' => NULL_ALLOWED, ], 'isdefault' => [ 'type' => PARAM_BOOL, 'description' => 'Whether is the default collection.', 'default' => false, ], 'component' => [ 'type' => PARAM_COMPONENT, 'description' => 'Component the collection is related to.', 'null' => NULL_ALLOWED, ], 'sortorder' => [ 'type' => PARAM_INT, 'description' => 'Collection ordering in the list.', ], 'searchable' => [ 'type' => PARAM_BOOL, 'description' => 'Whether the tag collection is searchable.', 'default' => true, ], 'customurl' => [ 'type' => PARAM_NOTAGS, 'description' => 'Custom URL for the tag page instead of /tag/index.php.', 'null' => NULL_ALLOWED, ], ]; } } task/cron_task.php 0000644 00000004010 15151222372 0010175 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A scheduled task for chat cron. * * @package mod_chat * @copyright 2019 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_chat\task; defined('MOODLE_INTERNAL') || die(); /** * The main schedule task for the chat module. * * @package mod_chat * @copyright 2019 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cron_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('crontask', 'mod_chat'); } /** * Run chat cron. */ public function execute() { global $CFG, $DB; require_once($CFG->dirroot . '/mod/chat/lib.php'); chat_update_chat_times(); chat_delete_old_users(); $timenow = time(); $subselect = "SELECT c.keepdays FROM {chat} c WHERE c.id = {chat_messages}.chatid"; $DB->delete_records_select('chat_messages', "($subselect) > 0 AND timestamp < (? - ($subselect) * ?)", [$timenow, DAYSECS]); $DB->delete_records_select('chat_messages_current', "timestamp < ?", [$timenow - 8 * HOURSECS]); } } output/user_groups_editable.php 0000644 00000014115 15151251034 0013022 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_group\output\user_groups_editable * * @package core_group * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\output; use context_course; use core_user; use core_external; use coding_exception; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/group/lib.php'); /** * Class to display list of user groups. * * @package core_group * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_groups_editable extends \core\output\inplace_editable { /** @var $coursegroups */ private $coursegroups = null; /** @var $context */ private $context = null; /** * Constructor. * * @param \stdClass $course The current course * @param \context $context The course context * @param \stdClass $user The current user * @param \stdClass[] $coursegroups The list of course groups from groups_get_all_groups with membership. * @param array $value Array of groupids. */ public function __construct($course, $context, $user, $coursegroups, $value) { // Check capabilities to get editable value. $editable = has_capability('moodle/course:managegroups', $context) && !empty($coursegroups); // Invent an itemid. $itemid = $course->id . ':' . $user->id; $value = json_encode($value); // Remember these for the display value. $this->coursegroups = $coursegroups; $this->context = $context; parent::__construct('core_group', 'user_groups', $itemid, $editable, $value, $value); // Assignable groups. $options = []; foreach ($coursegroups as $group) { $options[$group->id] = format_string($group->name, true, ['context' => $this->context]); } $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $this->context)); $this->edithint = get_string('editusersgroupsa', 'group', $fullname); $this->editlabel = get_string('editusersgroupsa', 'group', $fullname); $attributes = ['multiple' => true]; $this->set_type_autocomplete($options, $attributes); } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output * @return array */ public function export_for_template(\renderer_base $output) { $listofgroups = []; $groupids = json_decode($this->value); foreach ($groupids as $id) { $listofgroups[] = format_string($this->coursegroups[$id]->name, true, ['context' => $this->context]); } if (!empty($listofgroups)) { $this->displayvalue = implode(', ', $listofgroups); } else { $this->displayvalue = get_string('groupsnone'); } return parent::export_for_template($output); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { // Check caps. // Do the thing. // Return one of me. // Validate the inputs. list($courseid, $userid) = explode(':', $itemid, 2); $courseid = clean_param($courseid, PARAM_INT); $userid = clean_param($userid, PARAM_INT); $groupids = json_decode($newvalue); foreach ($groupids as $index => $groupid) { $groupids[$index] = clean_param($groupid, PARAM_INT); } // Check user is enrolled in the course. $context = context_course::instance($courseid); core_external::validate_context($context); if (!is_enrolled($context, $userid)) { throw new coding_exception('User does not belong to the course'); } // Check that all the groups belong to the course. $coursegroups = groups_get_all_groups($courseid, 0, 0, 'g.*', true); $byid = []; foreach ($groupids as $groupid) { if (!isset($coursegroups[$groupid])) { throw new coding_exception('Group does not belong to the course'); } $byid[$groupid] = $groupid; } $groupids = $byid; // Check permissions. require_capability('moodle/course:managegroups', $context); // Process adds. foreach ($groupids as $groupid) { if (!isset($coursegroups[$groupid]->members[$userid])) { // Add them. groups_add_member($groupid, $userid); // Keep this variable in sync. $coursegroups[$groupid]->members[$userid] = $userid; } } // Process removals. foreach ($coursegroups as $groupid => $group) { if (isset($group->members[$userid]) && !isset($groupids[$groupid])) { if (groups_remove_member_allowed($groupid, $userid)) { groups_remove_member($groupid, $userid); unset($coursegroups[$groupid]->members[$userid]); } else { $groupids[$groupid] = $groupid; } } } $course = get_course($courseid); $user = core_user::get_user($userid); return new self($course, $context, $user, $coursegroups, array_values($groupids)); } } output/group_details.php 0000644 00000006274 15151251034 0011464 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Group details page. * * @package core_group * @copyright 2017 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use context_course; use moodle_url; /** * Group details page class. * * @package core_group * @copyright 2017 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_details implements renderable, templatable { /** @var stdClass $group An object with the group information. */ protected $group; /** * group_details constructor. * * @param int $groupid Group ID to show details of. */ public function __construct($groupid) { $this->group = groups_get_group($groupid, '*', MUST_EXIST); } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { if (!empty($this->group->description) || (!empty($this->group->picture))) { $context = context_course::instance($this->group->courseid); $description = file_rewrite_pluginfile_urls($this->group->description, 'pluginfile.php', $context->id, 'group', 'description', $this->group->id); $descriptionformat = $this->group->descriptionformat ?? FORMAT_MOODLE; $options = [ 'overflowdiv' => true, 'context' => $context ]; $data = new stdClass(); $data->name = format_string($this->group->name, true, ['context' => $context]); $data->pictureurl = get_group_picture_url($this->group, $this->group->courseid, true); $data->description = format_text($description, $descriptionformat, $options); if (has_capability('moodle/course:managegroups', $context)) { $url = new moodle_url('/group/group.php', ['id' => $this->group->id, 'courseid' => $this->group->courseid]); $data->editurl = $url->out(false); } return $data; } else { return; } } } output/renderer.php 0000644 00000010552 15151251034 0010423 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace report_progress\output; use single_select; use plugin_renderer_base; use html_writer; /** * Renderer for report progress. * * @package report_progress * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ class renderer extends plugin_renderer_base { /** * Render include activity single select box. * * @param \moodle_url $url The base url. * @param array $activitytypes The activity type options. * @param string $activityinclude The current selected option. * @return string HTML * @throws \coding_exception */ public function render_include_activity_select(\moodle_url $url, array $activitytypes, string $activityinclude): string { $includeurl = fullclone($url); $includeurl->remove_params(['page', 'activityinclude']); $activityincludeselect = new single_select( $url, 'activityinclude', $activitytypes, $activityinclude, null, 'include-activity-select-report' ); $activityincludeselect->set_label(get_string('include', 'report_progress')); return \html_writer::div($this->output->render($activityincludeselect), 'include-activity-selector d-inline-block mr-3' ); } /** * Render activity order single select box. * * @param \moodle_url $url The base url. * @param string $activityorder The current selected option. * @return string HTML * @throws \coding_exception */ public function render_activity_order_select(\moodle_url $url, string $activityorder): string { $activityorderurl = fullclone($url); $activityorderurl->remove_params(['activityorder']); $options = ['orderincourse' => get_string('orderincourse', 'report_progress'), 'alphabetical' => get_string('alphabetical', 'report_progress')]; $sorttable = new single_select( $activityorderurl, 'activityorder', $options, $activityorder, null, 'activity-order-select-report' ); $sorttable->set_label(get_string('activityorder', 'report_progress')); return \html_writer::div($this->output->render($sorttable), 'activity-order-selector include-activity-selector d-inline-block'); } /** * Render groups single select box. * * @param \moodle_url $url The base url. * @param \stdClass $course Current course. * @return string HTML */ public function render_groups_select(\moodle_url $url, \stdClass $course): string { $groupurl = fullclone($url); $groupurl->remove_params(['page', 'group']); $groupoutput = groups_print_course_menu($course, $groupurl, true); if (empty($groupoutput)) { return $groupoutput; } return \html_writer::div($groupoutput, 'd-inline-block mr-3'); } /** * Render download buttons. * * @param \moodle_url $url The base url. * @return string HTML * @throws \coding_exception */ public function render_download_buttons(\moodle_url $url): string { $downloadurl = fullclone($url); $downloadurl->remove_params(['page']); $downloadurl->param('format', 'csv'); $downloadhtml = html_writer::start_tag('ul', ['class' => 'progress-actions']); $downloadhtml .= html_writer::tag('li', html_writer::link($downloadurl, get_string('csvdownload', 'completion'))); $downloadurl->param('format', 'excelcsv'); $downloadhtml .= html_writer::tag('li', html_writer::link($downloadurl, get_string('excelcsvdownload', 'completion'))); $downloadhtml .= html_writer::end_tag('ul'); return $downloadhtml; } } output/index_page.php 0000644 00000007567 15151251034 0010734 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Group index page. * * @package core_group * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_group\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; /** * Group index page class. * * @package core_group * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class index_page implements renderable, templatable { /** @var int $courseid The course ID. */ public $courseid; /** @var array The array of groups to be rendered. */ public $groups; /** @var string The name of the currently selected group. */ public $selectedgroupname; /** @var array The array of group members to be rendered, if a group is selected. */ public $selectedgroupmembers; /** @var bool Whether to disable the add members/edit group buttons. */ public $disableaddedit; /** @var bool Whether to disable the delete group button. */ public $disabledelete; /** @var array Groups that can't be deleted by the user. */ public $undeletablegroups; /** * index_page constructor. * * @param int $courseid The course ID. * @param array $groups The array of groups to be rendered. * @param string $selectedgroupname The name of the currently selected group. * @param array $selectedgroupmembers The array of group members to be rendered, if a group is selected. * @param bool $disableaddedit Whether to disable the add members/edit group buttons. * @param bool $disabledelete Whether to disable the delete group button. * @param array $undeletablegroups Groups that can't be deleted by the user. */ public function __construct($courseid, $groups, $selectedgroupname, $selectedgroupmembers, $disableaddedit, $disabledelete, $undeletablegroups) { $this->courseid = $courseid; $this->groups = $groups; $this->selectedgroupname = $selectedgroupname; $this->selectedgroupmembers = $selectedgroupmembers; $this->disableaddedit = $disableaddedit; $this->disabledelete = $disabledelete; $this->undeletablegroups = $undeletablegroups; } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $CFG; $data = new stdClass(); // Variables that will be passed to the JS helper. $data->courseid = $this->courseid; $data->wwwroot = $CFG->wwwroot; // To be passed to the JS init script in the template. Encode as a JSON string. $data->undeletablegroups = json_encode($this->undeletablegroups); // Some buttons are enabled if single group selected. $data->addmembersdisabled = $this->disableaddedit; $data->editgroupsettingsdisabled = $this->disableaddedit; $data->deletegroupdisabled = $this->disabledelete; $data->groups = $this->groups; $data->members = $this->selectedgroupmembers; $data->selectedgroup = $this->selectedgroupname; return $data; } } reportbuilder/datasource/groups.php 0000644 00000011334 15151251034 0013607 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\datasource; use core_group\reportbuilder\local\entities\{grouping, group, group_member}; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_reportbuilder\local\helpers\database; /** * Groups datasource * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class groups extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('groups', 'core_group'); } /** * Initialise report */ protected function initialise(): void { $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->set_main_table('course', $coursealias); $this->add_entity($courseentity); $paramsiteid = database::generate_param_name(); $this->add_base_condition_sql("{$coursealias}.id != :{$paramsiteid}", [$paramsiteid => SITEID]); // Re-use the context table alias/join from the course entity in subsequent entities. $contextalias = $courseentity->get_table_alias('context'); $this->add_join($courseentity->get_context_join()); // Group entity. $groupentity = (new group()) ->set_table_alias('context', $contextalias); $groupsalias = $groupentity->get_table_alias('groups'); $this->add_entity($groupentity ->add_join("LEFT JOIN {groups} {$groupsalias} ON {$groupsalias}.courseid = {$coursealias}.id")); // Grouping entity. $groupingentity = (new grouping()) ->set_table_alias('context', $contextalias); $groupingsalias = $groupingentity->get_table_alias('groupings'); // Sub-select for all groupings groups. $groupinginnerselect = " SELECT gr.*, grg.groupid FROM {groupings} gr JOIN {groupings_groups} grg ON grg.groupingid = gr.id"; $this->add_entity($groupingentity ->add_joins($groupentity->get_joins()) ->add_join("LEFT JOIN ({$groupinginnerselect}) {$groupingsalias} ON {$groupingsalias}.courseid = {$coursealias}.id AND {$groupingsalias}.groupid = {$groupsalias}.id")); // Group member entity. $groupmemberentity = new group_member(); $groupsmembersalias = $groupmemberentity->get_table_alias('groups_members'); $this->add_entity($groupmemberentity ->add_joins($groupentity->get_joins()) ->add_join("LEFT JOIN {groups_members} {$groupsmembersalias} ON {$groupsmembersalias}.groupid = {$groupsalias}.id")); // User entity. $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($groupmemberentity->get_joins()) ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$groupsmembersalias}.userid")); // Add all elements from entities to be available in custom reports. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'course:coursefullnamewithlink', 'group:name', 'user:fullname', ]; } /** * Return the filters that will be added to the report as part of default setup * * @return string[] */ public function get_default_filters(): array { return [ 'course:fullname', 'group:name', ]; } /** * Return the conditions that will be added to the report as part of default setup * * @return string[] */ public function get_default_conditions(): array { return [ 'course:fullname', 'group:name', ]; } } reportbuilder/local/entities/group.php 0000644 00000020741 15151251034 0014212 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use html_writer; use lang_string; use moodle_url; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'context' => 'gctx', 'groups' => 'g', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('group', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); $groupsalias = $this->get_table_alias('groups'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.name, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $group): string { if ($name === null) { return ''; } context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); return format_string($group->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.idnumber") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$groupsalias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$groupsalias}.descriptionformat, {$groupsalias}.id, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(false) ->set_callback(static function(?string $description, stdClass $group): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'group', 'description', $group->id); return format_text($description, $group->descriptionformat, ['context' => $context]); }); // Enrolment key column. $columns[] = (new column( 'enrolmentkey', new lang_string('enrolmentkey', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.enrolmentkey") ->set_is_sortable(true); // Picture column. $columns[] = (new column( 'picture', new lang_string('picture'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$groupsalias}.picture, {$groupsalias}.id, {$contextalias}.id AS contextid") ->set_is_sortable(false) // It doesn't make sense to offer integer aggregation methods for this column. ->set_disabled_aggregation(['avg', 'max', 'min', 'sum']) ->set_callback(static function ($picture, stdClass $group): string { if (empty($group->picture)) { return ''; } $pictureurl = moodle_url::make_pluginfile_url($group->contextid, 'group', 'icon', $group->id, '/', 'f2'); $pictureurl->param('rev', $group->picture); return html_writer::img($pictureurl, ''); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsalias = $this->get_table_alias('groups'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupsalias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } reportbuilder/local/entities/grouping.php 0000644 00000016276 15151251034 0014720 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group member entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grouping extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'context' => 'ggctx', 'groupings' => 'gg', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('grouping', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); $groupingsalias = $this->get_table_alias('groupings'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.name, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $grouping): string { if ($name === null) { return ''; } context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); return format_string($grouping->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.idnumber") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$groupingsalias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$groupingsalias}.descriptionformat, {$groupingsalias}.id, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(false) ->set_callback(static function(?string $description, stdClass $grouping): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'grouping', 'description', $grouping->id); return format_text($description, $grouping->descriptionformat, ['context' => $context]); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupingsalias = $this->get_table_alias('groupings'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupingsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupingsalias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupingsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } reportbuilder/local/entities/group_member.php 0000644 00000007404 15151251034 0015542 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use core_reportbuilder\local\filters\date; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group member entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_member extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'groups_members' => 'gm', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('groupmember', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added column. $columns[] = (new column( 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsmembersalias}.timeadded") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Component column. $columns[] = (new column( 'component', new lang_string('plugin', 'core'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsmembersalias}.component") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added filter. $filters[] = (new filter( date::class, 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsmembersalias}.timeadded" )) ->add_joins($this->get_joins()); return $filters; } } analytics/indicator/activity_base.php 0000644 00000002717 15151251270 0014054 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Activity base class. * * @package mod_resource * @copyright 2017 onwards Ankit Agarwal <ankit.agrr@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_resource\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Activity base class. * * @package mod_resource * @copyright 2017 onwards Ankit Agarwal <ankit.agrr@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { /** * No need to fetch grades for resources. * * @param \core_analytics\course $course * @return void */ public function fetch_student_grades(\core_analytics\course $course) { } } analytics/indicator/social_breadth.php 0000644 00000003326 15151251270 0014166 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Social breadth indicator - resource. * * @package mod_resource * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_resource\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Social breadth indicator - resource. * * @package mod_resource * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name() : \lang_string { return new \lang_string('indicator:socialbreadth', 'mod_resource'); } public function get_indicator_type() { return self::INDICATOR_SOCIAL; } public function get_social_breadth_level(\cm_info $cm) { return self::SOCIAL_LEVEL_1; } } analytics/indicator/cognitive_depth.php 0000644 00000003341 15151251270 0014373 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Cognitive depth indicator - resource. * * @package mod_resource * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_resource\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Cognitive depth indicator - resource. * * @package mod_resource * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name() : \lang_string { return new \lang_string('indicator:cognitivedepth', 'mod_resource'); } public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } public function get_cognitive_depth_level(\cm_info $cm) { return self::COGNITIVE_LEVEL_1; } } output/registration_upgrade_choice_page.php 0000644 00000004314 15151251270 0015345 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for rendering LTI upgrade choices page. * * @copyright 2021 Cengage * @package mod_lti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use renderable; use templatable; use renderer_base; use stdClass; /** * Class containing data for rendering LTI upgrade choices page. * * @copyright 2021 Cengage * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class registration_upgrade_choice_page implements renderable, templatable { /** * Constructor * * @param array $tools array of tools that can be upgraded * @param string $startregurl tool URL to start the registration process */ public function __construct(array $tools, string $startregurl) { $this->tools = $tools; $this->startregurl = $startregurl; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass Data to be used by the template */ public function export_for_template(renderer_base $output) { $renderdata = new stdClass(); $renderdata->startregurlenc = urlencode($this->startregurl); $renderdata->sesskey = sesskey(); $renderdata->tools = []; foreach ($this->tools as $tool) { $renderdata->tools[] = (object)$tool; } return $renderdata; } } output/external_registration_return_page.php 0000644 00000003140 15151251270 0015621 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for external registration return page. * * @package mod_lti * @copyright 2015 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use renderable; use templatable; use renderer_base; use stdClass; /** * Class containing data for tool_configure page * * @copyright 2015 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_registration_return_page implements renderable, templatable { /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass Data to be used by the template */ public function export_for_template(renderer_base $output) { return new stdClass(); } } output/tool_configure_page.php 0000644 00000004421 15151251270 0012627 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for tool_configure page * * @package mod_lti * @copyright 2015 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use moodle_url; use renderable; use templatable; use renderer_base; use stdClass; use help_icon; /** * Class containing data for tool_configure page * * @copyright 2015 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_configure_page implements renderable, templatable { /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $keyhelp = new help_icon('resourcekey', 'mod_lti'); $secrethelp = new help_icon('password', 'mod_lti'); $url = new moodle_url('/mod/lti/typessettings.php', array('sesskey' => sesskey(), 'returnto' => 'toolconfigure')); $data->configuremanualurl = $url->out(); $url = new moodle_url('/admin/settings.php?section=modsettinglti'); $data->managetoolsurl = $url->out(); $url = new moodle_url('/mod/lti/toolproxies.php'); $data->managetoolproxiesurl = $url->out(); $data->keyhelp = $keyhelp->export_for_template($output); $data->secrethelp = $secrethelp->export_for_template($output); return $data; } } output/repost_crosssite_page.php 0000644 00000005146 15151251270 0013230 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Render a page containing a simple form which reposts to self via JS. * * The purpose of this form is to resend a cross-site request to self, which allows the browsers to include the Moodle * session cookie alongside the original POST data, allowing LTI flows to function despite browsers blocking * cross-site cookies. * * @copyright 2021 Cengage * @package mod_lti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\output; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/mod/lti/locallib.php'); use renderable; use templatable; use renderer_base; use stdClass; /** * Render a page containing a simple form which reposts to self via JS. * * The purpose of this form is to resend a cross-site request to self, which allows the browsers to include the Moodle * session cookie alongside the original POST data, allowing LTI flows to function despite browsers blocking * cross-site cookies. * * @copyright 2021 Cengage * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repost_crosssite_page implements renderable, templatable { /** * Constructor * * @param string $url moodle URL to repost to * @param array $post the POST params to be re-posted */ public function __construct(string $url, array $post) { $this->params = array_map(function($k) use ($post) { return ["key" => $k, "value" => $post[$k]]; }, array_keys($post)); $this->url = $url; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output The renderer * @return stdClass Data to be used by the template */ public function export_for_template(renderer_base $output) { $renderdata = new stdClass(); $renderdata->url = $this->url; $renderdata->params = $this->params; return $renderdata; } } plugininfo/ltisource.php 0000644 00000005423 15151251270 0011443 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * LTI source plugin info. * * @package mod_lti * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\plugininfo; use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); class ltisource extends base { /** * Returns the node name used in admin settings menu for this plugin settings (if applicable) * * @return null|string node name or null if plugin does not create settings node (default) */ public function get_settings_section_name() { return 'ltisourcesetting'.$this->name; } /** * Loads plugin settings to the settings tree * * This function usually includes settings.php file in plugins folder. * Alternatively it can create a link to some settings page (instance of admin_externalpage) * * @param \part_of_admin_tree $adminroot * @param string $parentnodename * @param bool $hassiteconfig whether the current user has moodle/site:config capability */ public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) { global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them. $ADMIN = $adminroot; // May be used in settings.php. $plugininfo = $this; // Also can be used inside settings.php. if (!$this->is_installed_and_upgraded()) { return; } if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) { return; } $section = $this->get_settings_section_name(); $settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false); include($this->full_path('settings.php')); // This may also set $settings to null. if ($settings) { $ADMIN->add($parentnodename, $settings); } } /** * Should there be a way to uninstall the plugin via the administration UI. * * @return bool */ public function is_uninstall_allowed() { return true; } } plugininfo/ltiservice.php 0000644 00000003150 15151251270 0011576 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * LTI service plugin info. * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\plugininfo; use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); /** * The mod_lti\plugininfo\ltiservice class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ltiservice extends base { /** * Should there be a way to uninstall the plugin via the administration UI? * * Uninstallation is not allowed for core subplugins. * * @return boolean */ public function is_uninstall_allowed() { if ($this->is_standard()) { return false; } return true; } } helper.php 0000644 00000011360 15151251270 0006534 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Content bank files repository helpers. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank; use repository_contentbank\browser\contentbank_browser; /** * Helper class for content bank files repository. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Get the content bank repository browser for a certain context. * * @param \context $context The context * @return \repository_contentbank\browser\contentbank_browser|null The content bank repository browser */ public static function get_contentbank_browser(\context $context): ?contentbank_browser { switch ($context->contextlevel) { case CONTEXT_SYSTEM: return new \repository_contentbank\browser\contentbank_browser_context_system($context); case CONTEXT_COURSECAT: return new \repository_contentbank\browser\contentbank_browser_context_coursecat($context); case CONTEXT_COURSE: return new \repository_contentbank\browser\contentbank_browser_context_course($context); } return null; } /** * Create the context folder node. * * @param string $name The name of the context folder node * @param string $path The path to the context folder node * @return array The context folder node */ public static function create_context_folder_node(string $name, string $path): array { global $OUTPUT; return [ 'title' => $name, 'datemodified' => '', 'datecreated' => '', 'path' => $path, 'thumbnail' => $OUTPUT->image_url(file_folder_icon(90))->out(false), 'children' => [] ]; } /** * Create the content bank content node. * * @param \core_contentbank\content $content The content bank content * @return array|null The content bank content node */ public static function create_contentbank_content_node(\core_contentbank\content $content): ?array { global $OUTPUT; // Only content files are currently supported, but should be able to create content folder nodes in the future. // Early return if the content is not a stored file. if (!$file = $content->get_file()) { return null; } $params = [ 'contextid' => $file->get_contextid(), 'component' => $file->get_component(), 'filearea' => $file->get_filearea(), 'itemid' => $file->get_itemid(), 'filepath' => $file->get_filepath(), 'filename' => $file->get_filename() ]; $contenttype = $content->get_content_type_instance(); $encodedpath = base64_encode(json_encode($params)); $node = [ 'shorttitle' => $content->get_name(), 'title' => $file->get_filename(), 'datemodified' => $file->get_timemodified(), 'datecreated' => $file->get_timecreated(), 'author' => $file->get_author(), 'license' => $file->get_license(), 'isref' => $file->is_external_file(), 'size' => $file->get_filesize(), 'source' => $encodedpath, 'icon' => $contenttype->get_icon($content), 'thumbnail' => $contenttype->get_icon($content) ]; if ($file->get_status() == 666) { $node['originalmissing'] = true; } return $node; } /** * Generate a navigation node. * * @param \context $context The context * @return array The navigation node */ public static function create_navigation_node(\context $context): array { return [ 'path' => base64_encode(json_encode(['contextid' => $context->id])), 'name' => $context->get_context_name(false) ]; } } event/unknown_service_api_called.php 0000644 00000005701 15151251270 0013754 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_lti unknown service api called event. * * @package mod_lti * @copyright 2013 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_lti unknown service api called event class. * * Event for when something happens with an unknown lti service API call. * * @package mod_lti * @since Moodle 2.6 * @copyright 2013 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class unknown_service_api_called extends \core\event\base { /** @var \stdClass Data to be used by event observers. */ protected $eventdata; /** * Sets custom data used by event observers. * * @param \stdClass $data */ public function set_message_data(\stdClass $data) { $this->eventdata = $data; } /** * Returns custom data for event observers. * * @return \stdClass */ public function get_message_data() { if ($this->is_restored()) { throw new \coding_exception('Function get_message_data() can not be used on restored events.'); } return $this->eventdata; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->context = \context_system::instance(); } /** * Returns localised description of what happened. * * @return string */ public function get_description() { return 'An unknown call to a service api was made.'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('ltiunknownserviceapicall', 'mod_lti'); } /** * Does this event replace a legacy event? * * @return null|string legacy event name */ public static function get_legacy_eventname() { return 'lti_unknown_service_api_call'; } /** * Legacy event data if get_legacy_eventname() is not empty. * * @return mixed */ protected function get_legacy_eventdata() { return $this->eventdata; } } event/course_module_instance_list_viewed.php 0000644 00000002565 15151251270 0015534 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_resource instance list viewed event. * * @package mod_resource * @copyright 2014 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_resource\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_resource instance list viewed event class. * * @package mod_resource * @since Moodle 2.7 * @copyright 2014 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { // No need for any code here as everything is handled by the parent class. } event/course_module_viewed.php 0000644 00000003177 15151251270 0012615 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_resource course module viewed event. * * @package mod_resource * @copyright 2014 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_resource\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_resource course module viewed event class. * * @package mod_resource * @since Moodle 2.7 * @copyright 2014 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'resource'; $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } public static function get_objectid_mapping() { return array('db' => 'resource', 'restore' => 'resource'); } } task/clean_access_tokens.php 0000644 00000003101 15151251270 0012177 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A scheduled task for lti module. * * @package mod_lti * @copyright 2019 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\task; use core\task\scheduled_task; defined('MOODLE_INTERNAL') || die(); /** * Class containing the scheduled task for lti module. * * @package mod_lti * @copyright 2018 Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class clean_access_tokens extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('cleanaccesstokens', 'mod_lti'); } /** * Run lti cron. */ public function execute() { global $DB; $DB->delete_records_select('lti_access_tokens', 'validuntil < ?', [time()]); } } local/ltiopenid/jwks_helper.php 0000644 00000010720 15151251270 0012652 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This files exposes functions for LTI 1.3 Key Management. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiopenid; use Firebase\JWT\JWT; /** * This class exposes functions for LTI 1.3 Key Management. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class jwks_helper { /** * * See https://www.imsglobal.org/spec/security/v1p1#approved-jwt-signing-algorithms. * @var string[] */ private static $ltisupportedalgs = [ 'RS256' => 'RSA', 'RS384' => 'RSA', 'RS512' => 'RSA', 'ES256' => 'EC', 'ES384' => 'EC', 'ES512' => 'EC' ]; /** * Returns the private key to use to sign outgoing JWT. * * @return array keys are kid and key in PEM format. */ public static function get_private_key() { $privatekey = get_config('mod_lti', 'privatekey'); $kid = get_config('mod_lti', 'kid'); return [ "key" => $privatekey, "kid" => $kid ]; } /** * Returns the JWK Key Set for this site. * @return array keyset exposting the site public key. */ public static function get_jwks() { $jwks = array('keys' => array()); $privatekey = self::get_private_key(); $res = openssl_pkey_get_private($privatekey['key']); $details = openssl_pkey_get_details($res); $jwk = array(); $jwk['kty'] = 'RSA'; $jwk['alg'] = 'RS256'; $jwk['kid'] = $privatekey['kid']; $jwk['e'] = rtrim(strtr(base64_encode($details['rsa']['e']), '+/', '-_'), '='); $jwk['n'] = rtrim(strtr(base64_encode($details['rsa']['n']), '+/', '-_'), '='); $jwk['use'] = 'sig'; $jwks['keys'][] = $jwk; return $jwks; } /** * Take an array of JWKS keys and infer the 'alg' property for a single key, if missing, based on an input JWT. * * This only sets the 'alg' property for a single key when all the following conditions are met: * - The key's 'kid' matches the 'kid' provided in the JWT's header. * - The key's 'alg' is missing. * - The JWT's header 'alg' matches the algorithm family of the key (the key's kty). * - The JWT's header 'alg' matches one of the approved LTI asymmetric algorithms. * * Keys not matching the above are left unchanged. * * @param array $jwks the keyset array. * @param string $jwt the JWT string. * @return array the fixed keyset array. */ public static function fix_jwks_alg(array $jwks, string $jwt): array { $jwtparts = explode('.', $jwt); $jwtheader = json_decode(JWT::urlsafeB64Decode($jwtparts[0]), true); if (!isset($jwtheader['kid'])) { throw new \moodle_exception('Error: kid must be provided in JWT header.'); } foreach ($jwks['keys'] as $index => $key) { // Only fix the key being referred to in the JWT. if ($jwtheader['kid'] != $key['kid']) { continue; } // Only fix the key if the alg is missing. if (!empty($key['alg'])) { continue; } // The header alg must match the key type (family) specified in the JWK's kty. if (!isset(static::$ltisupportedalgs[$jwtheader['alg']]) || static::$ltisupportedalgs[$jwtheader['alg']] != $key['kty']) { throw new \moodle_exception('Error: Alg specified in the JWT header is incompatible with the JWK key type'); } $jwks['keys'][$index]['alg'] = $jwtheader['alg']; } return $jwks; } } local/ltiopenid/registration_exception.php 0000644 00000002174 15151251270 0015131 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This library exposes functions for LTI Dynamic Registration. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiopenid; /** * Exception when transforming the registration to LTI config. * * Code is the HTTP Error code. */ class registration_exception extends \Exception { } local/ltiopenid/registration_helper.php 0000644 00000045560 15151251270 0014420 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A Helper for LTI Dynamic Registration. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiopenid; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/mod/lti/locallib.php'); use Firebase\JWT\JWK; use Firebase\JWT\JWT; use Firebase\JWT\Key; use stdClass; /** * This class exposes functions for LTI Dynamic Registration. * * @package mod_lti * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class registration_helper { /** score scope */ const SCOPE_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; /** result scope */ const SCOPE_RESULT = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; /** lineitem read-only scope */ const SCOPE_LINEITEM_RO = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; /** lineitem full access scope */ const SCOPE_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; /** Names and Roles (membership) scope */ const SCOPE_NRPS = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; /** Tool Settings scope */ const SCOPE_TOOL_SETTING = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting'; /** Indicates the token is to create a new registration */ const REG_TOKEN_OP_NEW_REG = 'reg'; /** Indicates the token is to update an existing registration */ const REG_TOKEN_OP_UPDATE_REG = 'reg-update'; /** * Get an instance of this helper * * @return object */ public static function get() { return new registration_helper(); } /** * Function used to validate parameters. * * This function is needed because the payload contains nested * objects, and optional_param() does not support arrays of arrays. * * @param array $payload that may contain the parameter key * @param string $key the key of the value to be looked for in the payload * @param bool $required if required, not finding a value will raise a registration_exception * * @return mixed */ private function get_parameter(array $payload, string $key, bool $required) { if (!isset($payload[$key]) || empty($payload[$key])) { if ($required) { throw new registration_exception('missing required attribute '.$key, 400); } return null; } $parameter = $payload[$key]; // Cleans parameters to avoid XSS and other issues. if (is_array($parameter)) { return clean_param_array($parameter, PARAM_TEXT, true); } return clean_param($parameter, PARAM_TEXT); } /** * Transforms an LTI 1.3 Registration to a Moodle LTI Config. * * @param array $registrationpayload the registration data received from the tool. * @param string $clientid the clientid to be issued for that tool. * * @return object the Moodle LTI config. */ public function registration_to_config(array $registrationpayload, string $clientid): object { $responsetypes = $this->get_parameter($registrationpayload, 'response_types', true); $initiateloginuri = $this->get_parameter($registrationpayload, 'initiate_login_uri', true); $redirecturis = $this->get_parameter($registrationpayload, 'redirect_uris', true); $clientname = $this->get_parameter($registrationpayload, 'client_name', true); $jwksuri = $this->get_parameter($registrationpayload, 'jwks_uri', true); $tokenendpointauthmethod = $this->get_parameter($registrationpayload, 'token_endpoint_auth_method', true); $applicationtype = $this->get_parameter($registrationpayload, 'application_type', false); $logouri = $this->get_parameter($registrationpayload, 'logo_uri', false); $ltitoolconfiguration = $this->get_parameter($registrationpayload, 'https://purl.imsglobal.org/spec/lti-tool-configuration', true); $domain = $this->get_parameter($ltitoolconfiguration, 'domain', false); $targetlinkuri = $this->get_parameter($ltitoolconfiguration, 'target_link_uri', false); $customparameters = $this->get_parameter($ltitoolconfiguration, 'custom_parameters', false); $scopes = explode(" ", $this->get_parameter($registrationpayload, 'scope', false) ?? ''); $claims = $this->get_parameter($ltitoolconfiguration, 'claims', false); $messages = $ltitoolconfiguration['messages'] ?? []; $description = $this->get_parameter($ltitoolconfiguration, 'description', false); // Validate domain and target link. if (empty($domain)) { throw new registration_exception('missing_domain', 400); } $targetlinkuri = $targetlinkuri ?: 'https://'.$domain; // Stripping www as this is ignored for domain matching. $domain = lti_get_domain_from_url($domain); if ($domain !== lti_get_domain_from_url($targetlinkuri)) { throw new registration_exception('domain_targetlinkuri_mismatch', 400); } // Validate response type. // According to specification, for this scenario, id_token must be explicitly set. if (!in_array('id_token', $responsetypes)) { throw new registration_exception('invalid_response_types', 400); } // According to specification, this parameter needs to be an array. if (!is_array($redirecturis)) { throw new registration_exception('invalid_redirect_uris', 400); } // According to specification, for this scenario private_key_jwt must be explicitly set. if ($tokenendpointauthmethod !== 'private_key_jwt') { throw new registration_exception('invalid_token_endpoint_auth_method', 400); } if (!empty($applicationtype) && $applicationtype !== 'web') { throw new registration_exception('invalid_application_type', 400); } $config = new stdClass(); $config->lti_clientid = $clientid; $config->lti_toolurl = $targetlinkuri; $config->lti_tooldomain = $domain; $config->lti_typename = $clientname; $config->lti_description = $description; $config->lti_ltiversion = LTI_VERSION_1P3; $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID; $config->lti_icon = $logouri; $config->lti_coursevisible = LTI_COURSEVISIBLE_PRECONFIGURED; $config->lti_contentitem = 0; // Sets Content Item. if (!empty($messages)) { $messagesresponse = []; foreach ($messages as $value) { if ($value['type'] === 'LtiDeepLinkingRequest') { $config->lti_contentitem = 1; $config->lti_toolurl_ContentItemSelectionRequest = $value['target_link_uri'] ?? ''; array_push($messagesresponse, $value); } } } $config->lti_keytype = 'JWK_KEYSET'; $config->lti_publickeyset = $jwksuri; $config->lti_initiatelogin = $initiateloginuri; $config->lti_redirectionuris = implode(PHP_EOL, $redirecturis); $config->lti_customparameters = ''; // Sets custom parameters. if (isset($customparameters)) { $paramssarray = []; foreach ($customparameters as $key => $value) { array_push($paramssarray, $key . '=' . $value); } $config->lti_customparameters = implode(PHP_EOL, $paramssarray); } // Sets launch container. $config->lti_launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; // Sets Service info based on scopes. $config->lti_acceptgrades = LTI_SETTING_NEVER; $config->ltiservice_gradesynchronization = 0; $config->ltiservice_memberships = 0; $config->ltiservice_toolsettings = 0; if (isset($scopes)) { // Sets Assignment and Grade Services info. if (in_array(self::SCOPE_SCORE, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 1; } if (in_array(self::SCOPE_RESULT, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 1; } if (in_array(self::SCOPE_LINEITEM_RO, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 1; } if (in_array(self::SCOPE_LINEITEM, $scopes)) { $config->lti_acceptgrades = LTI_SETTING_DELEGATE; $config->ltiservice_gradesynchronization = 2; } // Sets Names and Role Provisioning info. if (in_array(self::SCOPE_NRPS, $scopes)) { $config->ltiservice_memberships = 1; } // Sets Tool Settings info. if (in_array(self::SCOPE_TOOL_SETTING, $scopes)) { $config->ltiservice_toolsettings = 1; } } // Sets privacy settings. $config->lti_sendname = LTI_SETTING_NEVER; $config->lti_sendemailaddr = LTI_SETTING_NEVER; if (isset($claims)) { // Sets name privacy settings. if (in_array('name', $claims)) { $config->lti_sendname = LTI_SETTING_ALWAYS; } if (in_array('given_name', $claims)) { $config->lti_sendname = LTI_SETTING_ALWAYS; } if (in_array('family_name', $claims)) { $config->lti_sendname = LTI_SETTING_ALWAYS; } // Sets email privacy settings. if (in_array('email', $claims)) { $config->lti_sendemailaddr = LTI_SETTING_ALWAYS; } } return $config; } /** * Adds to the config the LTI 1.1 key and sign it with the 1.1 secret. * * @param array $lticonfig reference to lticonfig to which to add the 1.1 OAuth info. * @param string $key - LTI 1.1 OAuth Key * @param string $secret - LTI 1.1 OAuth Secret * */ private function add_previous_key_claim(array &$lticonfig, string $key, string $secret) { if ($key) { $oauthconsumer = []; $oauthconsumer['key'] = $key; $oauthconsumer['nonce'] = random_string(random_int(10, 20)); $oauthconsumer['sign'] = hash('sha256', $key.$secret.$oauthconsumer['nonce']); $lticonfig['oauth_consumer'] = $oauthconsumer; } } /** * Transforms a moodle LTI 1.3 Config to an OAuth/LTI Client Registration. * * @param object $config Moodle LTI Config. * @param int $typeid which is the LTI deployment id. * @param object $type tool instance in case the tool already exists. * * @return array the Client Registration as an associative array. */ public function config_to_registration(object $config, int $typeid, object $type = null): array { $configarray = []; foreach ((array)$config as $k => $v) { if (substr($k, 0, 4) == 'lti_') { $k = substr($k, 4); } $configarray[$k] = $v; } $config = (object) $configarray; $registrationresponse = []; $lticonfigurationresponse = []; $ltiversion = $type ? $type->ltiversion : $config->ltiversion; $lticonfigurationresponse['version'] = $ltiversion; if ($ltiversion === LTI_VERSION_1P3) { $registrationresponse['client_id'] = $type ? $type->clientid : $config->clientid; $registrationresponse['response_types'] = ['id_token']; $registrationresponse['jwks_uri'] = $config->publickeyset; $registrationresponse['initiate_login_uri'] = $config->initiatelogin; $registrationresponse['grant_types'] = ['client_credentials', 'implicit']; $registrationresponse['redirect_uris'] = explode(PHP_EOL, $config->redirectionuris); $registrationresponse['application_type'] = 'web'; $registrationresponse['token_endpoint_auth_method'] = 'private_key_jwt'; } else if ($ltiversion === LTI_VERSION_1 && $type) { $this->add_previous_key_claim($lticonfigurationresponse, $config->resourcekey, $config->password); } else if ($ltiversion === LTI_VERSION_2 && $type) { $toolproxy = $this->get_tool_proxy($type->toolproxyid); $this->add_previous_key_claim($lticonfigurationresponse, $toolproxy['guid'], $toolproxy['secret']); } $registrationresponse['client_name'] = $type ? $type->name : $config->typename; $registrationresponse['logo_uri'] = $type ? ($type->secureicon ?? $type->icon ?? '') : $config->icon ?? ''; $lticonfigurationresponse['deployment_id'] = strval($typeid); $lticonfigurationresponse['target_link_uri'] = $type ? $type->baseurl : $config->toolurl ?? ''; $lticonfigurationresponse['domain'] = $type ? $type->tooldomain : $config->tooldomain ?? ''; $lticonfigurationresponse['description'] = $type ? $type->description ?? '' : $config->description ?? ''; if ($config->contentitem ?? 0 == 1) { $contentitemmessage = []; $contentitemmessage['type'] = 'LtiDeepLinkingRequest'; if (isset($config->toolurl_ContentItemSelectionRequest)) { $contentitemmessage['target_link_uri'] = $config->toolurl_ContentItemSelectionRequest; } $lticonfigurationresponse['messages'] = [$contentitemmessage]; } if (isset($config->customparameters) && !empty($config->customparameters)) { $params = []; foreach (explode(PHP_EOL, $config->customparameters) as $param) { $split = explode('=', $param); $params[$split[0]] = $split[1]; } $lticonfigurationresponse['custom_parameters'] = $params; } $scopesresponse = []; if ($config->ltiservice_gradesynchronization ?? 0 > 0) { $scopesresponse[] = self::SCOPE_SCORE; $scopesresponse[] = self::SCOPE_RESULT; $scopesresponse[] = self::SCOPE_LINEITEM_RO; } if ($config->ltiservice_gradesynchronization ?? 0 == 2) { $scopesresponse[] = self::SCOPE_LINEITEM; } if ($config->ltiservice_memberships ?? 0 == 1) { $scopesresponse[] = self::SCOPE_NRPS; } if ($config->ltiservice_toolsettings ?? 0 == 1) { $scopesresponse[] = self::SCOPE_TOOL_SETTING; } $registrationresponse['scope'] = implode(' ', $scopesresponse); $claimsresponse = ['sub', 'iss']; if ($config->sendname ?? '' == LTI_SETTING_ALWAYS) { $claimsresponse[] = 'name'; $claimsresponse[] = 'family_name'; $claimsresponse[] = 'given_name'; } if ($config->sendemailaddr ?? '' == LTI_SETTING_ALWAYS) { $claimsresponse[] = 'email'; } $lticonfigurationresponse['claims'] = $claimsresponse; $registrationresponse['https://purl.imsglobal.org/spec/lti-tool-configuration'] = $lticonfigurationresponse; return $registrationresponse; } /** * Validates the registration token is properly signed and not used yet. * Return the client id to use for this registration. * * @param string $registrationtokenjwt registration token * * @return array with 2 keys: clientid for the registration, type but only if it's an update */ public function validate_registration_token(string $registrationtokenjwt): array { global $DB; // JWK::parseKeySet uses RS256 algorithm by default. $keys = JWK::parseKeySet(jwks_helper::get_jwks()); $registrationtoken = JWT::decode($registrationtokenjwt, $keys); $response = []; // Get clientid from registrationtoken. $clientid = $registrationtoken->sub; if ($registrationtoken->scope == self::REG_TOKEN_OP_NEW_REG) { // Checks if clientid is already registered. if (!empty($DB->get_record('lti_types', array('clientid' => $clientid)))) { throw new registration_exception("token_already_used", 401); } $response['clientid'] = $clientid; } else if ($registrationtoken->scope == self::REG_TOKEN_OP_UPDATE_REG) { $tool = lti_get_type($registrationtoken->sub); if (!$tool) { throw new registration_exception("Unknown client", 400); } $response['clientid'] = $tool->clientid ?? $this->new_clientid(); $response['type'] = $tool; } else { throw new registration_exception("Incorrect scope", 403); } return $response; } /** * Initializes an array with the scopes for services supported by the LTI module * * @return array List of scopes */ public function lti_get_service_scopes() { $services = lti_get_services(); $scopes = array(); foreach ($services as $service) { $servicescopes = $service->get_scopes(); if (!empty($servicescopes)) { $scopes = array_merge($scopes, $servicescopes); } } return $scopes; } /** * Generates a new client id string. * * @return string generated client id */ public function new_clientid(): string { return random_string(15); } /** * Base64 encoded signature for LTI 1.1 migration. * @param string $key LTI 1.1 key * @param string $salt Salt value * @param string $secret LTI 1.1 secret * * @return string base64encoded hash */ public function sign(string $key, string $salt, string $secret): string { return base64_encode(hash_hmac('sha-256', $key.$salt, $secret, true)); } /** * Returns a tool proxy * * @param int $proxyid * * @return mixed Tool Proxy details */ public function get_tool_proxy(int $proxyid) : array { return lti_get_tool_proxy($proxyid); } } local/ltiservice/service_base.php 0000644 00000036735 15151251270 0013167 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains an abstract definition of an LTI service * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiservice; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_once($CFG->dirroot . '/mod/lti/OAuthBody.php'); // TODO: Switch to core oauthlib once implemented - MDL-30149. use moodle\mod\lti as lti; use stdClass; /** * The mod_lti\local\ltiservice\service_base class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class service_base { /** Label representing an LTI 2 message type */ const LTI_VERSION2P0 = 'LTI-2p0'; /** Service enabled */ const SERVICE_ENABLED = 1; /** @var string ID for the service. */ protected $id; /** @var string Human readable name for the service. */ protected $name; /** @var boolean <code>true</code> if requests for this service do not need to be signed. */ protected $unsigned; /** @var stdClass Tool proxy object for the current service request. */ private $toolproxy; /** @var stdClass LTI type object for the current service request. */ private $type; /** @var array LTI type config array for the current service request. */ private $typeconfig; /** @var array Instances of the resources associated with this service. */ protected $resources; /** * Class constructor. */ public function __construct() { $this->id = null; $this->name = null; $this->unsigned = false; $this->toolproxy = null; $this->type = null; $this->typeconfig = null; $this->resources = null; } /** * Get the service ID. * * @return string */ public function get_id() { return $this->id; } /** * Get the service compoent ID. * * @return string */ public function get_component_id() { return 'ltiservice_' . $this->id; } /** * Get the service name. * * @return string */ public function get_name() { return $this->name; } /** * Get whether the service requests need to be signed. * * @return boolean */ public function is_unsigned() { return $this->unsigned; } /** * Get the tool proxy object. * * @return stdClass */ public function get_tool_proxy() { return $this->toolproxy; } /** * Set the tool proxy object. * * @param object $toolproxy The tool proxy for this service request * * @var stdClass */ public function set_tool_proxy($toolproxy) { $this->toolproxy = $toolproxy; } /** * Get the type object. * * @return stdClass */ public function get_type() { return $this->type; } /** * Set the LTI type object. * * @param object $type The LTI type for this service request * * @var stdClass */ public function set_type($type) { $this->type = $type; } /** * Get the type config array. * * @return array|null */ public function get_typeconfig() { return $this->typeconfig; } /** * Set the LTI type config object. * * @param array $typeconfig The LTI type config for this service request * * @var array */ public function set_typeconfig($typeconfig) { $this->typeconfig = $typeconfig; } /** * Get the resources for this service. * * @return resource_base[] */ abstract public function get_resources(); /** * Get the scope(s) permitted for this service in the context of a particular tool type. * * A null value indicates that no scopes are required to access the service. * * @return array|null */ public function get_permitted_scopes() { return null; } /** * Get the scope(s) permitted for this service. * * A null value indicates that no scopes are required to access the service. * * @return array|null */ public function get_scopes() { return null; } /** * Returns the configuration options for this service. * * @param \MoodleQuickForm $mform Moodle quickform object definition */ public function get_configuration_options(&$mform) { } /** * Called when a new LTI Instance is added. * * @param object $lti LTI Instance. */ public function instance_added(object $lti): void { } /** * Called when a new LTI Instance is updated. * * @param object $lti LTI Instance. */ public function instance_updated(object $lti): void { } /** * Called when the launch data is created, offering a possibility to alter the * target link URI. * * @param string $messagetype message type for this launch * @param string $targetlinkuri current target link uri * @param null|string $customstr concatenated list of custom parameters * @param int $courseid * @param null|object $lti LTI Instance. * * @return array containing the target link URL and the custom params string to use. */ public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid, ?object $lti = null): array { return [$targetlinkuri, $customstr]; } /** * Called when a new LTI Instance is deleted. * * @param int $id LTI Instance. */ public function instance_deleted(int $id): void { } /** * Set the form data when displaying the LTI Instance form. * * @param object $defaultvalues Default form values. */ public function set_instance_form_values(object $defaultvalues): void { } /** * Return an array with the names of the parameters that the service will be saving in the configuration * * @return array Names list of the parameters that the service will be saving in the configuration * @deprecated since Moodle 3.7 - please do not use this function any more. */ public function get_configuration_parameter_names() { debugging('get_configuration_parameter_names() has been deprecated.', DEBUG_DEVELOPER); return array(); } /** * Default implementation will check for the existence of at least one mod_lti entry for that tool and context. * * It may be overridden if other inferences can be done. * * Ideally a Site Tool should be explicitly engaged with a course, the check on the presence of a link is a proxy * to infer a Site Tool engagement until an explicit Site Tool - Course relationship exists. * * @param int $typeid The tool lti type id. * @param int $courseid The course id. * @return bool returns True if tool is used in context, false otherwise. */ public function is_used_in_context($typeid, $courseid) { global $DB; $ok = $DB->record_exists('lti', array('course' => $courseid, 'typeid' => $typeid)); return $ok || $DB->record_exists('lti_types', array('course' => $courseid, 'id' => $typeid)); } /** * Checks if there is a site tool or a course tool for this site. * * @param int $typeid The tool lti type id. * @param int $courseid The course id. * @return bool returns True if tool is allowed in context, false otherwise. */ public function is_allowed_in_context($typeid, $courseid) { global $DB; // Check if it is a Course tool for this course or a Site tool. $type = $DB->get_record('lti_types', array('id' => $typeid)); return $type && ($type->course == $courseid || $type->course == SITEID); } /** * Return an array of key/values to add to the launch parameters. * * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'. * @param string $courseid The course id. * @param string $userid The user id. * @param string $typeid The tool lti type id. * @param string $modlti The id of the lti activity. * * The type is passed to check the configuration and not return parameters for services not used. * * @return array Key/value pairs to add as launch parameters. */ public function get_launch_parameters($messagetype, $courseid, $userid, $typeid, $modlti = null) { return array(); } /** * Return an array of key/claim mapping allowing LTI 1.1 custom parameters * to be transformed to LTI 1.3 claims. * * @return array Key/value pairs of params to claim mapping. */ public function get_jwt_claim_mappings(): array { return []; } /** * Get the path for service requests. * * @return string */ public static function get_service_path() { $url = new \moodle_url('/mod/lti/services.php'); return $url->out(false); } /** * Parse a string for custom substitution parameter variables supported by this service's resources. * * @param string $value Value to be parsed * * @return string */ public function parse_value($value) { if (empty($this->resources)) { $this->resources = $this->get_resources(); } if (!empty($this->resources)) { foreach ($this->resources as $resource) { $value = $resource->parse_value($value); } } return $value; } /** * Check that the request has been properly signed and is permitted. * * @param string $typeid LTI type ID * @param string $body Request body (null if none) * @param string[] $scopes Array of required scope(s) for incoming request * * @return boolean */ public function check_tool($typeid, $body = null, $scopes = null) { $ok = true; $toolproxy = null; $consumerkey = lti\get_oauth_key_from_headers($typeid, $scopes); if ($consumerkey === false) { $ok = $this->is_unsigned(); } else { if (empty($typeid) && is_int($consumerkey)) { $typeid = $consumerkey; } if (!empty($typeid)) { $this->type = lti_get_type($typeid); $this->typeconfig = lti_get_type_config($typeid); $ok = !empty($this->type->id); if ($ok && !empty($this->type->toolproxyid)) { $this->toolproxy = lti_get_tool_proxy($this->type->toolproxyid); } } else { $toolproxy = lti_get_tool_proxy_from_guid($consumerkey); if ($toolproxy !== false) { $this->toolproxy = $toolproxy; } } } if ($ok && is_string($consumerkey)) { if (!empty($this->toolproxy)) { $key = $this->toolproxy->guid; $secret = $this->toolproxy->secret; } else { $key = $this->typeconfig['resourcekey']; $secret = $this->typeconfig['password']; } if (!$this->is_unsigned() && ($key == $consumerkey)) { $ok = $this->check_signature($key, $secret, $body); } else { $ok = $this->is_unsigned(); } } return $ok; } /** * Check that the request has been properly signed. * * @param string $toolproxyguid Tool Proxy GUID * @param string $body Request body (null if none) * * @return boolean * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see service_base::check_tool() */ public function check_tool_proxy($toolproxyguid, $body = null) { debugging('check_tool_proxy() is deprecated to allow LTI 1 connections to support services. ' . 'Please use service_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; $toolproxy = null; $consumerkey = lti\get_oauth_key_from_headers(); if (empty($toolproxyguid)) { $toolproxyguid = $consumerkey; } if (!empty($toolproxyguid)) { $toolproxy = lti_get_tool_proxy_from_guid($toolproxyguid); if ($toolproxy !== false) { if (!$this->is_unsigned() && ($toolproxy->guid == $consumerkey)) { $ok = $this->check_signature($toolproxy->guid, $toolproxy->secret, $body); } else { $ok = $this->is_unsigned(); } } } if ($ok) { $this->toolproxy = $toolproxy; } return $ok; } /** * Check that the request has been properly signed. * * @param int $typeid The tool id * @param int $courseid The course we are at * @param string $body Request body (null if none) * * @return bool * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see service_base::check_tool() */ public function check_type($typeid, $courseid, $body = null) { debugging('check_type() is deprecated to allow LTI 1 connections to support services. ' . 'Please use service_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; $tool = null; $consumerkey = lti\get_oauth_key_from_headers(); if (empty($typeid)) { return $ok; } else if ($this->is_allowed_in_context($typeid, $courseid)) { $tool = lti_get_type_type_config($typeid); if ($tool !== false) { if (!$this->is_unsigned() && ($tool->lti_resourcekey == $consumerkey)) { $ok = $this->check_signature($tool->lti_resourcekey, $tool->lti_password, $body); } else { $ok = $this->is_unsigned(); } } } return $ok; } /** * Check the request signature. * * @param string $consumerkey Consumer key * @param string $secret Shared secret * @param string $body Request body * * @return boolean */ private function check_signature($consumerkey, $secret, $body) { $ok = true; try { // TODO: Switch to core oauthlib once implemented - MDL-30149. lti\handle_oauth_body_post($consumerkey, $secret, $body); } catch (\Exception $e) { debugging($e->getMessage() . "\n"); $ok = false; } return $ok; } } local/ltiservice/response.php 0000644 00000015656 15151251270 0012372 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains an abstract definition of an LTI service * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiservice; defined('MOODLE_INTERNAL') || die; /** * The mod_lti\local\ltiservice\response class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class response { /** @var int HTTP response code. */ private $code; /** @var string HTTP response reason. */ private $reason; /** @var string HTTP request method. */ private $requestmethod; /** @var string HTTP request accept header. */ private $accept; /** @var string HTTP response content type. */ private $contenttype; /** @var string HTTP request body. */ private $data; /** @var string HTTP response body. */ private $body; /** @var array HTTP response codes. */ private $responsecodes; /** @var array HTTP additional headers. */ private $additionalheaders; /** * Class constructor. */ public function __construct() { $this->code = 200; $this->reason = ''; $this->requestmethod = $_SERVER['REQUEST_METHOD']; $this->accept = ''; $this->contenttype = ''; $this->data = ''; $this->body = ''; $this->responsecodes = array( 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 300 => 'Multiple Choices', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 415 => 'Unsupported Media Type', 500 => 'Internal Server Error', 501 => 'Not Implemented' ); $this->additionalheaders = array(); } /** * Get the response code. * * @return int */ public function get_code() { return $this->code; } /** * Set the response code. * * @param int $code Response code */ public function set_code($code) { $this->code = $code; $this->reason = ''; } /** * Get the response reason. * * @return string */ public function get_reason() { $code = $this->code; if (($code < 200) || ($code >= 600)) { $code = 500; // Status code must be between 200 and 599. } if (empty($this->reason) && array_key_exists($code, $this->responsecodes)) { $this->reason = $this->responsecodes[$code]; } // Use generic reason for this category (based on first digit) if a specific reason is not defined. if (empty($this->reason)) { $this->reason = $this->responsecodes[intval($code / 100) * 100]; } return $this->reason; } /** * Set the response reason. * * @param string $reason Reason */ public function set_reason($reason) { $this->reason = $reason; } /** * Get the request method. * * @return string */ public function get_request_method() { return $this->requestmethod; } /** * Get the request accept header. * * @return string */ public function get_accept() { return $this->accept; } /** * Set the request accept header. * * @param string $accept Accept header value */ public function set_accept($accept) { $this->accept = $accept; } /** * Get the response content type. * * @return string */ public function get_content_type() { return $this->contenttype; } /** * Set the response content type. * * @param string $contenttype Content type */ public function set_content_type($contenttype) { $this->contenttype = $contenttype; } /** * Get the request body. * * @return string */ public function get_request_data() { return $this->data; } /** * Set the response body. * * @param string $data Body data */ public function set_request_data($data) { $this->data = $data; } /** * Get the response body. * * @return string */ public function get_body() { return $this->body; } /** * Set the response body. * * @param string $body Body data */ public function set_body($body) { $this->body = $body; } /** * Add an additional header. * * @param string $header The new header */ public function add_additional_header($header) { array_push($this->additionalheaders, $header); } /** * Send the response. */ public function send() { header("HTTP/1.0 {$this->code} {$this->get_reason()}"); foreach ($this->additionalheaders as $header) { header($header); } if ((($this->code >= 200) && ($this->code < 300)) || !empty($this->body)) { if (!empty($this->contenttype)) { header("Content-Type: {$this->contenttype}; charset=utf-8"); } if (!empty($this->body)) { echo $this->body; } } else if ($this->code >= 400) { header("Content-Type: application/json; charset=utf-8"); $body = new \stdClass(); $body->status = $this->code; $body->reason = $this->get_reason(); $body->request = new \stdClass(); $body->request->method = $_SERVER['REQUEST_METHOD']; $body->request->url = $_SERVER['REQUEST_URI']; if (isset($_SERVER['HTTP_ACCEPT'])) { $body->request->accept = $_SERVER['HTTP_ACCEPT']; } if (isset($_SERVER['CONTENT_TYPE'])) { $body->request->contentType = explode(';', $_SERVER['CONTENT_TYPE'], 2)[0]; } echo json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } } } local/ltiservice/resource_base.php 0000644 00000027455 15151251270 0013355 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains an abstract definition of an LTI resource * * @package mod_lti * @copyright 2014 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti\local\ltiservice; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * The mod_lti\local\ltiservice\resource_base class. * * @package mod_lti * @since Moodle 2.8 * @copyright 2014 Vital Source Technologies http://vitalsource.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class resource_base { /** HTTP Post method */ const HTTP_POST = 'POST'; /** HTTP Get method */ const HTTP_GET = 'GET'; /** HTTP Put method */ const HTTP_PUT = 'PUT'; /** HTTP Delete method */ const HTTP_DELETE = 'DELETE'; /** @var service_base Service associated with this resource. */ private $service; /** @var string Type for this resource. */ protected $type; /** @var string ID for this resource. */ protected $id; /** @var string Template for this resource. */ protected $template; /** @var array Custom parameter substitution variables associated with this resource. */ protected $variables; /** @var array Media types supported by this resource. */ protected $formats; /** @var array HTTP actions supported by this resource. */ protected $methods; /** @var array Template variables parsed from the resource template. */ protected $params; /** * Class constructor. * * @param service_base $service Service instance */ public function __construct($service) { $this->service = $service; $this->type = 'RestService'; $this->id = null; $this->template = null; $this->methods = array(); $this->variables = array(); $this->formats = array(); $this->methods = array(); $this->params = null; } /** * Get the resource ID. * * @return string */ public function get_id() { return $this->id; } /** * Get the resource template. * * @return string */ public function get_template() { return $this->template; } /** * Get the resource path. * * @return string */ public function get_path() { return $this->get_template(); } /** * Get the resource type. * * @return string */ public function get_type() { return $this->type; } /** * Get the resource's service. * * @return mixed */ public function get_service() { return $this->service; } /** * Get the resource methods. * * @return array */ public function get_methods() { return $this->methods; } /** * Get the resource media types. * * @return array */ public function get_formats() { return $this->formats; } /** * Get the resource template variables. * * @return array */ public function get_variables() { return $this->variables; } /** * Get the resource fully qualified endpoint. * * @return string */ public function get_endpoint() { $this->parse_template(); $template = preg_replace('/[\(\)]/', '', $this->get_template()); $url = $this->get_service()->get_service_path() . $template; foreach ($this->params as $key => $value) { $url = str_replace('{' . $key . '}', $value, $url); } $toolproxy = $this->get_service()->get_tool_proxy(); if (!empty($toolproxy)) { $url = str_replace('{config_type}', 'toolproxy', $url); $url = str_replace('{tool_proxy_id}', $toolproxy->guid, $url); } else { $url = str_replace('{config_type}', 'tool', $url); $url = str_replace('{tool_proxy_id}', $this->get_service()->get_type()->id, $url); } return $url; } /** * Execute the request for this resource. * * @param response $response Response object for this request. */ public abstract function execute($response); /** * Check to make sure the request is valid. * * @param int $typeid The typeid we want to use * @param string $body Body of HTTP request message * @param string[] $scopes Array of scope(s) required for incoming request * * @return boolean */ public function check_tool($typeid, $body = null, $scopes = null) { $ok = $this->get_service()->check_tool($typeid, $body, $scopes); if ($ok) { if ($this->get_service()->get_tool_proxy()) { $toolproxyjson = $this->get_service()->get_tool_proxy()->toolproxy; } if (!empty($toolproxyjson)) { // Check tool proxy to ensure service being requested is included. $toolproxy = json_decode($toolproxyjson); if (!empty($toolproxy) && isset($toolproxy->security_contract->tool_service)) { $contexts = lti_get_contexts($toolproxy); $tpservices = $toolproxy->security_contract->tool_service; foreach ($tpservices as $service) { $fqid = lti_get_fqid($contexts, $service->service); $id = explode('#', $fqid, 2); if ($this->get_id() === $id[1]) { $ok = true; break; } } } if (!$ok) { debugging('Requested service not permitted: ' . $this->get_id(), DEBUG_DEVELOPER); } } else { // Check that the scope required for the service request is included in those granted for the // access token being used. $permittedscopes = $this->get_service()->get_permitted_scopes(); $ok = is_null($permittedscopes) || empty($scopes) || !empty(array_intersect($permittedscopes, $scopes)); } } return $ok; } /** * Check to make sure the request is valid. * * @param string $toolproxyguid Consumer key * @param string $body Body of HTTP request message * * @return boolean * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see resource_base::check_tool() */ public function check_tool_proxy($toolproxyguid, $body = null) { debugging('check_tool_proxy() is deprecated to allow LTI 1 connections to support services. ' . 'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; if ($this->get_service()->check_tool_proxy($toolproxyguid, $body)) { $toolproxyjson = $this->get_service()->get_tool_proxy()->toolproxy; if (empty($toolproxyjson)) { $ok = true; } else { $toolproxy = json_decode($toolproxyjson); if (!empty($toolproxy) && isset($toolproxy->security_contract->tool_service)) { $contexts = lti_get_contexts($toolproxy); $tpservices = $toolproxy->security_contract->tool_service; foreach ($tpservices as $service) { $fqid = lti_get_fqid($contexts, $service->service); $id = explode('#', $fqid, 2); if ($this->get_id() === $id[1]) { $ok = true; break; } } } if (!$ok) { debugging('Requested service not included in tool proxy: ' . $this->get_id()); } } } return $ok; } /** * Check to make sure the request is valid. * * @param int $typeid The typeid we want to use * @param int $contextid The course we are at * @param string $permissionrequested The permission to be checked * @param string $body Body of HTTP request message * * @return boolean * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see resource_base::check_tool() */ public function check_type($typeid, $contextid, $permissionrequested, $body = null) { debugging('check_type() is deprecated to allow LTI 1 connections to support services. ' . 'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER); $ok = false; if ($this->get_service()->check_type($typeid, $contextid, $body)) { $neededpermissions = $this->get_permissions($typeid); foreach ($neededpermissions as $permission) { if ($permission == $permissionrequested) { $ok = true; break; } } if (!$ok) { debugging('Requested service ' . $permissionrequested . ' not included in tool type: ' . $typeid, DEBUG_DEVELOPER); } } return $ok; } /** * get permissions from the config of the tool for that resource * * @param int $ltitype Type of LTI * @return array with the permissions related to this resource by the $ltitype or empty if none. * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see resource_base::check_tool() */ public function get_permissions($ltitype) { debugging('get_permissions() is deprecated to allow LTI 1 connections to support services. ' . 'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER); return array(); } /** * Parse a value for custom parameter substitution variables. * * @param string $value String to be parsed * * @return string */ public function parse_value($value) { return $value; } /** * Parse the template for variables. * * @return array */ protected function parse_template() { if (empty($this->params)) { $this->params = array(); if (!empty($_SERVER['PATH_INFO'])) { $path = explode('/', $_SERVER['PATH_INFO']); $template = preg_replace('/\([0-9a-zA-Z_\-,\/]+\)/', '', $this->get_template()); $parts = explode('/', $template); for ($i = 0; $i < count($parts); $i++) { if ((substr($parts[$i], 0, 1) == '{') && (substr($parts[$i], -1) == '}')) { $value = ''; if ($i < count($path)) { $value = $path[$i]; } $this->params[substr($parts[$i], 1, -1)] = $value; } } } } return $this->params; } } service_exception_handler.php 0000644 00000006236 15151251270 0012476 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Exception handler for LTI services * * @package mod_lti * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_lti; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../locallib.php'); require_once(__DIR__.'/../servicelib.php'); /** * Handles exceptions when handling incoming LTI messages. * * Ensures that LTI always returns a XML message that can be consumed by the caller. * * @package mod_lti * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class service_exception_handler { /** * Enable error response logging. * * @var bool */ protected $log = false; /** * The LTI service message ID, if known. * * @var string */ protected $id = ''; /** * The LTI service message type, if known. * * @var string */ protected $type = 'unknownRequest'; /** * Constructor. * * @param boolean $log Enable error response logging. */ public function __construct($log) { $this->log = $log; } /** * Set the LTI message ID being handled. * * @param string $id */ public function set_message_id($id) { if (!empty($id)) { $this->id = $id; } } /** * Set the LTI message type being handled. * * @param string $type */ public function set_message_type($type) { if (!empty($type)) { $this->type = $type; } } /** * Echo an exception message encapsulated in XML. * * @param \Exception|\Throwable $exception The exception that was thrown */ public function handle($exception) { $message = $exception->getMessage(); // Add the exception backtrace for developers. if (debugging('', DEBUG_DEVELOPER)) { $message .= "\n".format_backtrace(get_exception_info($exception)->backtrace, true); } // Switch to response. $type = str_replace('Request', 'Response', $this->type); // Build the appropriate xml. $response = lti_get_response_xml('failure', $message, $this->id, $type); $xml = $response->asXML(); // Log the request if necessary. if ($this->log) { lti_log_response($xml, $exception); } echo $xml; } } search/activity.php 0000644 00000003246 15151251270 0010362 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Search area for mod_resource activities. * * @package mod_resource * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_resource\search; defined('MOODLE_INTERNAL') || die(); /** * Search area for mod_resource activities. * * @package mod_resource * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } /** * Return the context info required to index files for * this search area. * * @return array */ public function get_search_fileareas() { $fileareas = array('intro', 'content'); // Fileareas. return $fileareas; } } external/get_tool_types_and_proxies_count.php 0000644 00000005575 15151251270 0015755 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace mod_lti\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * External function for fetching the count of all tool types and proxies. * * @package mod_lti * @author Andrew Madden <andrewmadden@catalyst-au.net> * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_tool_types_and_proxies_count extends \external_api { /** * Get parameter definition for get_tool_types_and_proxies_count(). * * @return \external_function_parameters */ public static function execute_parameters(): \external_function_parameters { return new \external_function_parameters( [ 'toolproxyid' => new \external_value(PARAM_INT, 'Tool proxy id', VALUE_DEFAULT, 0), 'orphanedonly' => new \external_value(PARAM_BOOL, 'Orphaned tool types only', VALUE_DEFAULT, 0), ] ); } /** * Get count of every tool type and tool proxy. * * @param int $toolproxyid The tool proxy id * @param bool $orphanedonly Whether to get orphaned proxies only. * @return array */ public static function execute($toolproxyid, $orphanedonly): array { $params = self::validate_parameters(self::execute_parameters(), [ 'toolproxyid' => $toolproxyid, 'orphanedonly' => $orphanedonly, ]); $toolproxyid = $params['toolproxyid']; $orphanedonly = $params['orphanedonly']; $context = \context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); return [ 'count' => lti_get_lti_types_and_proxies_count($orphanedonly, $toolproxyid), ]; } /** * Get return definition for get_tool_types_and_proxies_count. * * @return \external_single_structure */ public static function execute_returns(): \external_single_structure { return new \external_single_structure([ 'count' => new \external_value(PARAM_INT, 'Total number of tool types and proxies', VALUE_REQUIRED), ]); } } external/get_tool_types_and_proxies.php 0000644 00000007726 15151251270 0014545 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace mod_lti\external; defined('MOODLE_INTERNAL') || die(); /** * External function for fetching all tool types and proxies. * * @package mod_lti * @author Andrew Madden <andrewmadden@catalyst-au.net> * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_tool_types_and_proxies extends \external_api { /** * Get parameter definition for get_tool_types_and_proxies(). * * @return \external_function_parameters */ public static function execute_parameters(): \external_function_parameters { return new \external_function_parameters( [ 'toolproxyid' => new \external_value(PARAM_INT, 'Tool proxy id', VALUE_DEFAULT, 0), 'orphanedonly' => new \external_value(PARAM_BOOL, 'Orphaned tool types only', VALUE_DEFAULT, 0), 'limit' => new \external_value(PARAM_INT, 'How many tool types displayed per page', VALUE_DEFAULT, 60, NULL_NOT_ALLOWED), 'offset' => new \external_value(PARAM_INT, 'Current offset of tool elements', VALUE_DEFAULT, 0, NULL_NOT_ALLOWED), ] ); } /** * Get data for all tool types and tool proxies. * * @param int $toolproxyid The tool proxy id * @param bool $orphanedonly Whether to get orphaned proxies only. * @param int $limit How many elements to return if using pagination. * @param int $offset Which chunk of elements to return is using pagination. * @return array */ public static function execute($toolproxyid, $orphanedonly, $limit, $offset): array { $params = self::validate_parameters(self::execute_parameters(), [ 'toolproxyid' => $toolproxyid, 'orphanedonly' => $orphanedonly, 'limit' => $limit, 'offset' => $offset, ]); $toolproxyid = $params['toolproxyid'] !== null ? $params['toolproxyid'] : 0; $orphanedonly = $params['orphanedonly'] !== null ? $params['orphanedonly'] : false; $limit = $params['limit'] !== null ? $params['limit'] : 0; $offset = $params['offset'] !== null ? $params['offset'] : 0; $context = \context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); list($proxies, $types) = lti_get_lti_types_and_proxies($limit, $offset, $orphanedonly, $toolproxyid); return [ 'types' => $types, 'proxies' => $proxies, 'limit' => $limit, 'offset' => $offset, ]; } /** * Get return definition for get_tool_types_and_proxies. * * @return \external_single_structure */ public static function execute_returns(): \external_single_structure { return new \external_single_structure([ 'types' => \mod_lti_external::get_tool_types_returns(), 'proxies' => \mod_lti_external::get_tool_proxies_returns(), 'limit' => new \external_value(PARAM_INT, 'Limit of how many tool types to show', VALUE_OPTIONAL), 'offset' => new \external_value(PARAM_INT, 'Offset of tool types', VALUE_OPTIONAL), ]); } } privacy/legacy_polyfill.php 0000644 00000005155 15151251416 0012117 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up. * * @package core_plagiarism * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_plagiarism\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; /** * The trait used to provide a backwards compatibility for third-party plugins. * * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait legacy_polyfill { /** * Export all plagiarism data from each plagiarism plugin for the specified userid and context. * * @param int $userid The user to export. * @param \context $context The context to export. * @param array $subcontext The subcontext within the context to export this information to. * @param array $linkarray The weird and wonderful link array used to display information for a specific item */ public static function export_plagiarism_user_data(int $userid, \context $context, array $subcontext, array $linkarray) { static::_export_plagiarism_user_data($userid, $context, $subcontext, $linkarray); } /** * Delete all user information for the provided context. * * @param \context $context The context to delete user data for. */ public static function delete_plagiarism_for_context(\context $context) { static::_delete_plagiarism_for_context($context); } /** * Delete all user information for the provided user and context. * * @param int $userid The user to delete * @param \context $context The context to refine the deletion. */ public static function delete_plagiarism_for_user(int $userid, \context $context) { static::_delete_plagiarism_for_user($userid, $context); } } privacy/plagiarism_user_provider.php 0000644 00000003340 15151251416 0014033 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Interface for deleting users related to a context. * * @package core_plagiarism * @copyright 2018 Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_plagiarism\privacy; defined('MOODLE_INTERNAL') || die(); /** * Interface for the plagiarism system. * * @copyright 2018 Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface plagiarism_user_provider extends // The plagiarism_provider should be implemented by plugins which only provide information to a subsystem. \core_privacy\local\request\plugin\subsystem_provider, \core_privacy\local\request\shared_userlist_provider { /** * Delete all user information for the provided users and context. * * @param array $userids The users to delete * @param \context $context The context to refine the deletion. */ public static function delete_plagiarism_for_users(array $userids, \context $context); } privacy/plagiarism_provider.php 0000644 00000004706 15151251416 0013004 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package core_plagiarism * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_plagiarism\privacy; defined('MOODLE_INTERNAL') || die(); /** * Provider for the plagiarism API. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface plagiarism_provider extends // The plagiarism_provider should be implemented by plugins which only provide information to a subsystem. \core_privacy\local\request\plugin\subsystem_provider { /** * Export all plagiarism data from each plagiarism plugin for the specified userid and context. * * @param int $userid The user to export. * @param \context $context The context to export. * @param array $subcontext The subcontext within the context to export this information to. * @param array $linkarray The weird and wonderful link array used to display information for a specific item */ public static function export_plagiarism_user_data(int $userid, \context $context, array $subcontext, array $linkarray); /** * Delete all user information for the provided context. * * @param \context $context The context to delete user data for. */ public static function delete_plagiarism_for_context(\context $context); /** * Delete all user information for the provided user and context. * * @param int $userid The user to delete * @param \context $context The context to refine the deletion. */ public static function delete_plagiarism_for_user(int $userid, \context $context); } field_controller.php 0000644 00000006143 15151252213 0010604 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Customfields checkbox plugin * * @package customfield_checkbox * @copyright 2018 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace customfield_checkbox; defined('MOODLE_INTERNAL') || die; /** * Class field * * @package customfield_checkbox * @copyright 2018 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class field_controller extends \core_customfield\field_controller { /** * Plugin type */ const TYPE = 'checkbox'; /** * Add fields for editing a checkbox field. * * @param \MoodleQuickForm $mform */ public function config_form_definition(\MoodleQuickForm $mform) { $mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_checkbox')); $mform->setExpanded('header_specificsettings', true); $mform->addElement('selectyesno', 'configdata[checkbydefault]', get_string('checkedbydefault', 'customfield_checkbox')); $mform->setType('configdata[checkbydefault]', PARAM_BOOL); } /** * Validate the data on the field configuration form * * @param array $data from the add/edit profile field form * @param array $files * @return array associative array of error messages */ public function config_form_validation(array $data, $files = array()) : array { $errors = parent::config_form_validation($data, $files); if ($data['configdata']['uniquevalues']) { $errors['configdata[uniquevalues]'] = get_string('errorconfigunique', 'customfield_checkbox'); } return $errors; } /** * Does this custom field type support being used as part of the block_myoverview * custom field grouping? * @return bool */ public function supports_course_grouping(): bool { return true; } /** * If this field supports course grouping, then this function needs overriding to * return the formatted values for this. * @param array $values the used values that need formatting * @return array */ public function course_grouping_format_values($values): array { $name = $this->get_formatted_name(); return [ 1 => $name.': '.get_string('yes'), BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY => $name.': '.get_string('no'), ]; } } data_controller.php 0000644 00000005650 15151252213 0010434 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Customfield Checkbox plugin * * @package customfield_checkbox * @copyright 2018 Daniel Neis Araujo <daniel@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace customfield_checkbox; use core_customfield\api; use core_customfield\output\field_data; defined('MOODLE_INTERNAL') || die; /** * Class data * * @package customfield_checkbox * @copyright 2018 Daniel Neis Araujo <daniel@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_controller extends \core_customfield\data_controller { /** * Return the name of the field where the information is stored * @return string */ public function datafield() : string { return 'intvalue'; } /** * Add fields for editing a checkbox field. * * @param \MoodleQuickForm $mform */ public function instance_form_definition(\MoodleQuickForm $mform) { $field = $this->get_field(); $config = $field->get('configdata'); $elementname = $this->get_form_element_name(); // If checkbox is required (i.e. "agree to terms") then use 'checkbox' form element. // The advcheckbox element cannot be used for required fields because advcheckbox elements always provide a value. $isrequired = $field->get_configdata_property('required'); $mform->addElement($isrequired ? 'checkbox' : 'advcheckbox', $elementname, $this->get_field()->get_formatted_name()); $mform->setDefault($elementname, $config['checkbydefault']); $mform->setType($elementname, PARAM_BOOL); if ($isrequired) { $mform->addRule($elementname, null, 'required', null, 'client'); } } /** * Returns the default value as it would be stored in the database (not in human-readable format). * * @return mixed */ public function get_default_value() { return $this->get_field()->get_configdata_property('checkbydefault') ? 1 : 0; } /** * Returns value in a human-readable format * * @return mixed|null value or null if empty */ public function export_value() { $value = $this->get_value(); return $value ? get_string('yes') : get_string('no'); } } event/message_sent.php 0000644 00000005647 15151264176 0011077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_chat message sent event. * * @package mod_chat * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_chat\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_chat message sent event class. * * @package mod_chat * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class message_sent extends \core\event\base { /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->relateduserid' has sent a message in the chat with course module id '$this->contextinstanceid'."; } /** * Return legacy log data. * * @return array */ protected function get_legacy_logdata() { $message = $this->get_record_snapshot('chat_messages', $this->objectid); return array($this->courseid, 'chat', 'talk', 'view.php?id=' . $this->contextinstanceid, $message->chatid, $this->contextinstanceid, $this->relateduserid); } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventmessagesent', 'mod_chat'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/chat/view.php', array('id' => $this->contextinstanceid)); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'chat_messages'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_objectid_mapping() { return array('db' => 'chat_messages', 'restore' => 'chat_message'); } } event/sessions_viewed.php 0000644 00000006325 15151264176 0011625 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_chat sessions viewed event. * * @package mod_chat * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_chat\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_chat sessions viewed event class. * * @property-read array $other { * Extra information about the event. * * - int start: start of period. * - int end: end of period. * } * * @package mod_chat * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sessions_viewed extends \core\event\base { /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has viewed the sessions of the chat with course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array|null */ protected function get_legacy_logdata() { return array($this->courseid, 'chat', 'report', 'report.php?id=' . $this->contextinstanceid, $this->objectid, $this->contextinstanceid); } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsessionsviewed', 'mod_chat'); } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/chat/report.php', array('id' => $this->contextinstanceid)); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'chat'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['start'])) { throw new \coding_exception('The \'start\' value must be set in other.'); } if (!isset($this->other['end'])) { throw new \coding_exception('The \'end\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'chat', 'restore' => 'chat'); } public static function get_other_mapping() { // Nothing to map. return false; } } external/chat_message_exporter.php 0000644 00000005723 15151264176 0013471 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting a chat message. * * @package mod_chat * @copyright 2017 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_chat\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class for exporting a chat message. * * @copyright 2017 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class chat_message_exporter extends exporter { /** * Defines exporter properties. * * @return array */ protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'The message record id.', ), 'chatid' => array( 'type' => PARAM_INT, 'description' => 'The chat id.', 'default' => 0, ), 'userid' => array( 'type' => PARAM_INT, 'description' => 'The user who wrote the message.', 'default' => 0, ), 'groupid' => array( 'type' => PARAM_INT, 'description' => 'The group this message belongs to.', 'default' => 0, ), 'issystem' => array( 'type' => PARAM_BOOL, 'description' => 'Whether is a system message or not.', 'default' => false, ), 'message' => array( 'type' => PARAM_RAW, 'description' => 'The message text.', ), 'timestamp' => array( 'type' => PARAM_INT, 'description' => 'The message timestamp (indicates when the message was sent).', 'default' => 0, ), ); } /** * Defines related information. * * @return array */ protected static function define_related() { return array( 'context' => 'context', ); } /** * Get the formatting parameters for the name. * * @return array */ protected function get_format_parameters_for_message() { return [ 'component' => 'mod_chat', ]; } } dates.php 0000644 00000003364 15151264176 0006373 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the class for fetching the important dates in mod_chat for a given module instance and a user. * * @package mod_chat * @copyright 2021 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_chat; use core\activity_dates; /** * Class for fetching the important dates in mod_chat for a given module instance and a user. * */ class dates extends activity_dates { /** * Returns a list of important dates in mod_chat. * * @return array */ protected function get_dates(): array { $customdata = $this->cm->customdata; $chat = (object) $customdata; $chattime = $chat->chattime ?? 0; $now = time(); if (!empty($chat->schedule) && $chattime > $now) { return [ [ 'dataid' => 'chattime', 'label' => get_string('nextchattime', 'mod_chat'), 'timestamp' => (int) $chattime ] ]; } return []; } } output/cohortname.php 0000644 00000005165 15151264460 0010767 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_cohort\output\cohortname * * @package core_cohort * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_cohort\output; use lang_string; /** * Class to prepare a cohort name for display. * * @package core_cohort * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohortname extends \core\output\inplace_editable { /** * Constructor. * * @param stdClass $cohort */ public function __construct($cohort) { $cohortcontext = \context::instance_by_id($cohort->contextid); $editable = has_capability('moodle/cohort:manage', $cohortcontext); $displayvalue = format_string($cohort->name, true, array('context' => $cohortcontext)); parent::__construct('core_cohort', 'cohortname', $cohort->id, $editable, $displayvalue, $cohort->name, new lang_string('editcohortname', 'cohort'), new lang_string('newnamefor', 'cohort', $displayvalue)); } /** * Updates cohort name and returns instance of this object * * @param int $cohortid * @param string $newvalue * @return static */ public static function update($cohortid, $newvalue) { global $DB; $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST); $cohortcontext = \context::instance_by_id($cohort->contextid); \external_api::validate_context($cohortcontext); require_capability('moodle/cohort:manage', $cohortcontext); $newvalue = clean_param($newvalue, PARAM_TEXT); if (strval($newvalue) !== '') { $record = (object)array('id' => $cohort->id, 'name' => $newvalue, 'contextid' => $cohort->contextid); cohort_update_cohort($record); $cohort->name = $newvalue; } return new static($cohort); } } output/cohortidnumber.php 0000644 00000005241 15151264460 0011647 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_cohort\output\cohortidnumber * * @package core_cohort * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_cohort\output; use lang_string; /** * Class to prepare a cohort idnumber for display. * * @package core_cohort * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohortidnumber extends \core\output\inplace_editable { /** * Constructor. * * @param stdClass $cohort */ public function __construct($cohort) { $cohortcontext = \context::instance_by_id($cohort->contextid); $editable = has_capability('moodle/cohort:manage', $cohortcontext); $displayvalue = s($cohort->idnumber); // All idnumbers are plain text. parent::__construct('core_cohort', 'cohortidnumber', $cohort->id, $editable, $displayvalue, $cohort->idnumber, new lang_string('editcohortidnumber', 'cohort'), new lang_string('newidnumberfor', 'cohort', $displayvalue)); } /** * Updates cohort name and returns instance of this object * * @param int $cohortid * @param string $newvalue * @return static */ public static function update($cohortid, $newvalue) { global $DB; $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST); $cohortcontext = \context::instance_by_id($cohort->contextid); \external_api::validate_context($cohortcontext); require_capability('moodle/cohort:manage', $cohortcontext); if ($newvalue == '' || !$DB->record_exists_select('cohort', 'idnumber = ? AND id != ?', [$newvalue, $cohort->id])) { $record = (object) ['id' => $cohort->id, 'idnumber' => $newvalue, 'contextid' => $cohort->contextid]; cohort_update_cohort($record); $cohort->idnumber = $newvalue; } return new static($cohort); } } reportbuilder/datasource/cohorts.php 0000644 00000007101 15151264460 0013755 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_cohort\reportbuilder\datasource; use core_cohort\reportbuilder\local\entities\{cohort, cohort_member}; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\user; /** * Cohorts datasource * * @package core_cohort * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohorts extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('cohorts', 'core_cohort'); } /** * Initialise report */ protected function initialise(): void { $cohortentity = new cohort(); $cohorttablealias = $cohortentity->get_table_alias('cohort'); $this->set_main_table('cohort', $cohorttablealias); $this->add_entity($cohortentity); // Join the cohort member entity to the cohort entity. $cohortmemberentity = new cohort_member(); $cohortmembertablealias = $cohortmemberentity->get_table_alias('cohort_members'); $this->add_entity($cohortmemberentity ->add_join("LEFT JOIN {cohort_members} {$cohortmembertablealias} ON {$cohortmembertablealias}.cohortid = {$cohorttablealias}.id") ); // Join the user entity to the cohort member entity. $userentity = new user(); $usertablealias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($cohortmemberentity->get_joins()) ->add_join("LEFT JOIN {user} {$usertablealias} ON {$usertablealias}.id = {$cohortmembertablealias}.userid") ); // Add all columns/filters/conditions from entities to be available in custom reports. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'cohort:name', 'cohort:context', 'cohort:idnumber', 'cohort:description', ]; } /** * Return the filters that will be added to the report once is created * * @return string[] */ public function get_default_filters(): array { return ['cohort:context', 'cohort:name']; } /** * Return the conditions that will be added to the report once is created * * @return string[] */ public function get_default_conditions(): array { return []; } /** * Return the default sorting that will be added to the report once it is created * * @return array|int[] */ public function get_default_column_sorting(): array { return [ 'cohort:name' => SORT_ASC, ]; } } reportbuilder/local/entities/cohort.php 0000644 00000024645 15151264460 0014372 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_cohort\reportbuilder\local\entities; use context; use context_helper; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * Cohort entity * * @package core_cohort * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohort extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'cohort' => 'c', 'context' => 'chctx', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('cohort', 'core_cohort'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $tablealias = $this->get_table_alias('cohort'); $contextalias = $this->get_table_alias('context'); // Category/context column. $columns[] = (new column( 'context', new lang_string('category'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join("JOIN {context} {$contextalias} ON {$contextalias}.id = {$tablealias}.contextid") ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.contextid, " . context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->add_callback(static function($contextid, stdClass $cohort): string { context_helper::preload_from_record($cohort); return context::instance_by_id($cohort->contextid)->get_context_name(false); }); // Name column. $columns[] = (new column( 'name', new lang_string('name', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name") ->set_is_sortable(true); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.idnumber") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$tablealias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join("JOIN {context} {$contextalias} ON {$contextalias}.id = {$tablealias}.contextid") ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$tablealias}.descriptionformat, {$tablealias}.id, {$tablealias}.contextid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->add_callback(static function(?string $description, stdClass $cohort): string { global $CFG; require_once("{$CFG->libdir}/filelib.php"); if ($description === null) { return ''; } context_helper::preload_from_record($cohort); $context = context::instance_by_id($cohort->contextid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'cohort', 'description', $cohort->id); return format_text($description, $cohort->descriptionformat, ['context' => $context->id]); }); // Visible column. $columns[] = (new column( 'visible', new lang_string('visible', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$tablealias}.visible") ->set_is_sortable(true) ->set_callback([format::class, 'boolean_as_text']); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tablealias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tablealias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Component column. $columns[] = (new column( 'component', new lang_string('component', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.component") ->set_is_sortable(true) ->add_callback(static function(string $component): string { return empty($component) ? get_string('nocomponent', 'cohort') : get_string('pluginname', $component); }); // Theme column. $columns[] = (new column( 'theme', new lang_string('theme'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.theme") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { global $DB; $tablealias = $this->get_table_alias('cohort'); // Context filter. $filters[] = (new filter( select::class, 'context', new lang_string('category'), $this->get_entity_name(), "{$tablealias}.contextid" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { global $DB; // Load all contexts in which there are cohorts. $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); $contexts = $DB->get_records_sql(" SELECT DISTINCT {$ctxfields}, c.contextid FROM {context} ctx JOIN {cohort} c ON c.contextid = ctx.id"); // Transform context record into it's name (used as the filter options). return array_map(static function(stdClass $contextrecord): string { context_helper::preload_from_record($contextrecord); return context::instance_by_id($contextrecord->contextid) ->get_context_name(false); }, $contexts); }); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name', 'core_cohort'), $this->get_entity_name(), "{$tablealias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber', 'core_cohort'), $this->get_entity_name(), "{$tablealias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$tablealias}.timecreated" )) ->add_joins($this->get_joins()); // Description filter. $filters[] = (new filter( text::class, 'description', new lang_string('description'), $this->get_entity_name(), $DB->sql_cast_to_char("{$tablealias}.description") )) ->add_joins($this->get_joins()); return $filters; } } reportbuilder/local/entities/cohort_member.php 0000644 00000006606 15151264460 0015716 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_cohort\reportbuilder\local\entities; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * Cohort member entity * * @package core_cohort * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohort_member extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return ['cohort_members' => 'cm']; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('cohortmember', 'core_cohort'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('cohort_members'); // Time added column. $columns[] = (new column( 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tablealias}.timeadded") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('cohort_members'); // Time added filter. $filters[] = (new filter( date::class, 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name(), "{$tablealias}.timeadded" )) ->add_joins($this->get_joins()); return $filters; } } reportbuilder/local/systemreports/cohorts.php 0000644 00000025623 15151264460 0015671 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_cohort\reportbuilder\local\systemreports; use context; use context_coursecat; use context_system; use core_cohort\reportbuilder\local\entities\cohort; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\action; use core_reportbuilder\local\report\column; use html_writer; use lang_string; use moodle_url; use pix_icon; use core_reportbuilder\system_report; use stdClass; /** * Cohorts system report class implementation * * @package core_cohort * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohorts extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { // Our main entity, it contains all of the column definitions that we need. $cohortentity = new cohort(); $entitymainalias = $cohortentity->get_table_alias('cohort'); $this->set_main_table('cohort', $entitymainalias); $this->add_entity($cohortentity); // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entitymainalias}.id, {$entitymainalias}.contextid, {$entitymainalias}.visible"); // Check if report needs to show a specific category. $contextid = $this->get_parameter('contextid', 0, PARAM_INT); $showall = $this->get_parameter('showall', true, PARAM_BOOL); if (!$showall) { $paramcontextid = database::generate_param_name(); $this->add_base_condition_sql("{$entitymainalias}.contextid = :$paramcontextid", [$paramcontextid => $contextid]); } // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns($cohortentity); $this->add_filters(); $this->add_actions(); // Set if report can be downloaded. $this->set_downloadable(false); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { $contextid = $this->get_parameter('contextid', 0, PARAM_INT); if ($contextid) { $context = context::instance_by_id($contextid, MUST_EXIST); } else { $context = context_system::instance(); } return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $context); } /** * Adds the columns we want to display in the report * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. * * @param cohort $cohortentity */ public function add_columns(cohort $cohortentity): void { $entitymainalias = $cohortentity->get_table_alias('cohort'); $showall = $this->get_parameter('showall', false, PARAM_BOOL); // Category column. An extra callback is appended in order to extend the current column formatting. if ($showall) { $this->add_column_from_entity('cohort:context') ->add_callback(static function(string $value, stdClass $cohort): string { $context = context::instance_by_id($cohort->contextid); if ($context instanceof context_coursecat) { return html_writer::link(new moodle_url('/cohort/index.php', ['contextid' => $cohort->contextid]), $value); } return $value; }); } // Name column using the inplace editable component. $this->add_column(new column( 'editablename', new lang_string('name', 'core_cohort'), $cohortentity->get_entity_name() )) ->set_type(column::TYPE_TEXT) ->set_is_sortable(true) ->add_fields("{$entitymainalias}.name, {$entitymainalias}.id, {$entitymainalias}.contextid") ->add_callback(static function(string $name, stdClass $cohort): string { global $OUTPUT, $PAGE; $renderer = $PAGE->get_renderer('core'); $template = new \core_cohort\output\cohortname($cohort); return $renderer->render_from_template('core/inplace_editable', $template->export_for_template($OUTPUT)); }); // ID Number column using the inplace editable component. $this->add_column(new column( 'editableidnumber', new lang_string('idnumber', 'core_cohort'), $cohortentity->get_entity_name() )) ->set_type(column::TYPE_TEXT) ->set_is_sortable(true) ->add_fields("{$entitymainalias}.idnumber, {$entitymainalias}.id, {$entitymainalias}.contextid") ->add_callback(static function(?string $idnumber, stdClass $cohort): string { global $OUTPUT, $PAGE; $renderer = $PAGE->get_renderer('core'); $template = new \core_cohort\output\cohortidnumber($cohort); return $renderer->render_from_template('core/inplace_editable', $template->export_for_template($OUTPUT)); }); // Description column. $this->add_column_from_entity('cohort:description'); // Cohort size column using a custom SQL query to count cohort members. $cm = database::generate_param_name(); $sql = "(SELECT count($cm.id) as memberscount FROM {cohort_members} $cm WHERE $cm.cohortid = {$entitymainalias}.id)"; $this->add_column(new column( 'memberscount', new lang_string('memberscount', 'cohort'), $cohortentity->get_entity_name() )) ->set_type(column::TYPE_INTEGER) ->set_is_sortable(true) ->add_field($sql, 'memberscount'); // Component column. Override the display name of a column. $this->add_column_from_entity('cohort:component') ->set_title(new lang_string('source', 'core_plugin')); // It's possible to set a default initial sort direction for one column. $this->set_initial_sort_column('cohort:editablename', SORT_ASC); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $filters = [ 'cohort:name', 'cohort:idnumber', 'cohort:description', ]; $this->add_filters_from_entities($filters); } /** * Add the system report actions. An extra column will be appended to each row, containing all actions added here * * Note the use of ":id" placeholder which will be substituted according to actual values in the row */ protected function add_actions(): void { $contextid = $this->get_parameter('contextid', 0, PARAM_INT); $showall = $this->get_parameter('showall', true, PARAM_BOOL); $returnurl = (new moodle_url('/cohort/index.php', ['id' => ':id', 'contextid' => $contextid, 'showall' => $showall]))->out(false); // Hide action. It will be only shown if the property 'visible' is true and user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('/cohort/edit.php', ['id' => ':id', 'sesskey' => sesskey(), 'hide' => 1, 'returnurl' => $returnurl]), new pix_icon('t/show', '', 'core'), [], false, new lang_string('hide') ))->add_callback(function($row) { return $row->visible && has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Show action. It will be only shown if the property 'visible' is false and user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('/cohort/edit.php', ['id' => ':id', 'sesskey' => sesskey(), 'show' => 1, 'returnurl' => $returnurl]), new pix_icon('t/hide', '', 'core'), [], false, new lang_string('show') ))->add_callback(function($row) { return !$row->visible && has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Edit action. It will be only shown if user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('/cohort/edit.php', ['id' => ':id', 'returnurl' => $returnurl]), new pix_icon('t/edit', '', 'core'), [], false, new lang_string('edit') ))->add_callback(function($row) { return has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Delete action. It will be only shown if user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('/cohort/edit.php', ['id' => ':id', 'delete' => 1, 'returnurl' => $returnurl]), new pix_icon('t/delete', '', 'core'), [], false, new lang_string('delete') ))->add_callback(function($row) { return has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Assign members to cohort action. It will be only shown if user has 'moodle/cohort:assign' capabillity. $this->add_action((new action( new moodle_url('/cohort/assign.php', ['id' => ':id', 'returnurl' => $returnurl]), new pix_icon('i/users', '', 'core'), [], false, new lang_string('assign', 'core_cohort') ))->add_callback(function($row) { return has_capability('moodle/cohort:assign', context::instance_by_id($row->contextid)); })); } /** * CSS class for the row * * @param stdClass $row * @return string */ public function get_row_class(stdClass $row): string { return (!$row->visible) ? 'text-muted' : ''; } } reportbuilder/audience/cohortmember.php 0000644 00000012626 15151264460 0014415 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_cohort\reportbuilder\audience; use context; use context_system; use core_course_category; use stdClass; use core_reportbuilder\local\audiences\base; use core_reportbuilder\local\helpers\database; use MoodleQuickForm; /** * The backend class for Cohort member audience type * * @package core_reportbuilder * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohortmember extends base { /** * Adds audience's elements to the given mform * * @param MoodleQuickForm $mform The form to add elements to */ public function get_config_form(MoodleQuickForm $mform): void { $cohorts = self::get_cohorts(); $mform->addElement('autocomplete', 'cohorts', get_string('selectfromcohort', 'cohort'), $cohorts, ['multiple' => true]); $mform->addRule('cohorts', null, 'required', null, 'client'); } /** * Helps to build SQL to retrieve users that matches the current report audience * * @param string $usertablealias * @return array array of three elements [$join, $where, $params] */ public function get_sql(string $usertablealias): array { global $DB; $cm = database::generate_alias(); $cohorts = $this->get_configdata()['cohorts']; $prefix = database::generate_param_name() . '_'; [$insql, $inparams] = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED, $prefix); $join = "JOIN {cohort_members} {$cm} ON ({$cm}.userid = {$usertablealias}.id)"; return [$join, "{$cm}.cohortid " . $insql, $inparams]; } /** * Return user friendly name of this audience type * * @return string */ public function get_name(): string { return get_string('memberofcohort', 'cohort'); } /** * Return the description for the audience. * * @return string */ public function get_description(): string { global $DB; $cohortlist = []; $cohortids = $this->get_configdata()['cohorts']; $cohorts = $DB->get_records_list('cohort', 'id', $cohortids, 'name'); foreach ($cohorts as $cohort) { $cohortlist[] = format_string($cohort->name, true, ['context' => $cohort->contextid, 'escape' => false]); } return $this->format_description_for_multiselect($cohortlist); } /** * If the current user is able to add this audience. * * @return bool */ public function user_can_add(): bool { // Check system context first. if (has_capability('moodle/cohort:view', context_system::instance())) { return true; } // If there is at least one category with given permissions, user can add. return !empty(core_course_category::make_categories_list('moodle/cohort:view')); } /** * Returns if this audience type is available for the user * * Check if there are available cohorts in the system for this user to use. * * @return bool */ public function is_available(): bool { return !empty(self::get_cohorts()); } /** * If the current user is able to edit this audience. * * @return bool */ public function user_can_edit(): bool { global $DB; $canedit = true; $cohortids = $this->get_configdata()['cohorts']; $cohorts = $DB->get_records_list('cohort', 'id', $cohortids); foreach ($cohorts as $cohort) { $context = context::instance_by_id($cohort->contextid, MUST_EXIST); $canedit = $canedit && has_capability('moodle/cohort:view', $context); if ($canedit === false) { break; } } return $canedit; } /** * Cohorts selector. * * @return array */ private static function get_cohorts(): array { global $CFG; require_once($CFG->dirroot.'/cohort/lib.php'); $cohortslist = []; // Search cohorts user can view. $usercohorts = cohort_get_all_cohorts(0, 0); // The previous method doesn't check cohorts on system context. $syscontext = context_system::instance(); $cohorts = array_filter($usercohorts['cohorts'], static function(stdClass $cohort) use ($syscontext): bool { return ($cohort->contextid != $syscontext->id) || has_capability('moodle/cohort:view', $syscontext); }); foreach ($cohorts as $cohort) { $cohortslist[$cohort->id] = format_string($cohort->name, true, [ 'context' => $cohort->contextid, 'escape' => false, ]); } return $cohortslist; } } external/cohort_summary_exporter.php 0000644 00000005421 15151264460 0014110 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting a cohort summary from an stdClass. * * @package core_cohort * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_cohort\external; defined('MOODLE_INTERNAL') || die(); use renderer_base; /** * Class for exporting a cohort summary from an stdClass. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohort_summary_exporter extends \core\external\exporter { protected static function define_related() { // Cohorts can exist on a category context. return array('context' => '\\context'); } public static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, ), 'name' => array( 'type' => PARAM_TEXT, ), 'idnumber' => array( 'type' => PARAM_RAW, // ID numbers are plain text. 'default' => '', 'null' => NULL_ALLOWED ), 'description' => array( 'type' => PARAM_TEXT, 'default' => '', 'null' => NULL_ALLOWED ), 'descriptionformat' => array( 'type' => PARAM_INT, 'default' => FORMAT_HTML, 'null' => NULL_ALLOWED ), 'visible' => array( 'type' => PARAM_BOOL, ), 'theme' => array( 'type' => PARAM_THEME, 'null' => NULL_ALLOWED ) ); } public static function define_other_properties() { return array( 'contextname' => array( // The method context::get_context_name() already formats the string, and may return HTML. 'type' => PARAM_RAW ), ); } protected function get_other_values(renderer_base $output) { return array( 'contextname' => $this->related['context']->get_context_name() ); } } output/select_page.php 0000644 00000004403 15151264516 0011077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Select page renderable. * * @package tool_moodlenet * @copyright 2020 Mathew May {@link https://mathew.solutions} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\output; defined('MOODLE_INTERNAL') || die; use tool_moodlenet\local\import_info; /** * Select page renderable. * * @package tool_moodlenet * @copyright 2020 Mathew May {@link https://mathew.solutions} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class select_page implements \renderable, \templatable { /** @var import_info $importinfo resource and config information pertaining to an import. */ protected $importinfo; /** * Inits the Select page renderable. * * @param import_info $importinfo resource and config information pertaining to an import. */ public function __construct(import_info $importinfo) { $this->importinfo = $importinfo; } /** * Return the import info. * * @return import_info the import information. */ public function get_import_info(): import_info { return $this->importinfo; } /** * Export the data. * * @param \renderer_base $output * @return \stdClass */ public function export_for_template(\renderer_base $output): \stdClass { // Prepare the context object. return (object) [ 'name' => $this->importinfo->get_resource()->get_name(), 'type' => $this->importinfo->get_config()->type, 'cancellink' => new \moodle_url('/'), ]; } } profile_manager.php 0000644 00000032277 15151264516 0010430 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Profile manager class * * @package tool_moodlenet * @copyright 2020 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet; /** * Class for handling interaction with the moodlenet profile. * * @package tool_moodlenet * @copyright 2020 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_manager { /** * Get the mnet profile for a user. * * @param int $userid The ID for the user to get the profile form * @return moodlenet_user_profile or null. */ public static function get_moodlenet_user_profile(int $userid): ?moodlenet_user_profile { global $CFG; // Check for official profile. if (self::official_profile_exists()) { $user = \core_user::get_user($userid, 'moodlenetprofile'); try { $userprofile = $user->moodlenetprofile ? $user->moodlenetprofile : ''; return (isset($user)) ? new moodlenet_user_profile(s($userprofile), $userid) : null; } catch (\moodle_exception $e) { // If an exception is thrown, means there isn't a valid profile set. No need to log exception. return null; } } // Otherwise get hacked in user profile field. require_once($CFG->dirroot . '/user/profile/lib.php'); $profilefields = profile_get_user_fields_with_data($userid); foreach ($profilefields as $key => $field) { if ($field->get_category_name() == self::get_category_name() && $field->inputname == 'profile_field_mnetprofile') { try { return new moodlenet_user_profile(s($field->display_data()), $userid); } catch (\moodle_exception $e) { // If an exception is thrown, means there isn't a valid profile set. No need to log exception. return null; } } } return null; } /** * Save the moodlenet profile. * * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to save. */ public static function save_moodlenet_user_profile(moodlenet_user_profile $moodlenetprofile): void { global $CFG, $DB; // Do some cursory checks first to see if saving is possible. if (self::official_profile_exists()) { // All good. Let's save. $user = \core_user::get_user($moodlenetprofile->get_userid()); $user->moodlenetprofile = $moodlenetprofile->get_profile_name(); require_once($CFG->dirroot . '/user/lib.php'); \user_update_user($user, false, true); return; } $fielddata = self::get_user_profile_field(); $fielddata = self::validate_and_fix_missing_profile_items($fielddata); // Everything should be back to normal. Let's save. require_once($CFG->dirroot . '/user/profile/lib.php'); \profile_save_custom_fields($moodlenetprofile->get_userid(), [$fielddata->shortname => $moodlenetprofile->get_profile_name()]); } /** * Checks to see if the required user profile fields and categories are in place. If not it regenerates them. * * @param stdClass $fielddata The moodlenet profile field. * @return stdClass The same moodlenet profile field, with any necessary updates made. */ private static function validate_and_fix_missing_profile_items(\stdClass $fielddata): \stdClass { global $DB; if (empty((array) $fielddata)) { // We need to regenerate the category and field to store this data. if (!self::check_profile_category()) { $categoryid = self::create_user_profile_category(); self::create_user_profile_text_field($categoryid); } else { // We need the category id. $category = $DB->get_record('user_info_category', ['name' => self::get_category_name()]); self::create_user_profile_text_field($category->id); } $fielddata = self::get_user_profile_field(); } else { if (!self::check_profile_category($fielddata->categoryid)) { $categoryid = self::create_user_profile_category(); // Update the field to put it back into this category. $fielddata->categoryid = $categoryid; $DB->update_record('user_info_field', $fielddata); } } return $fielddata; } /** * Returns the user profile field table object. * * @return stdClass the moodlenet profile table object. False if no record found. */ private static function get_user_profile_field(): \stdClass { global $DB; $fieldname = self::get_profile_field_name(); $record = $DB->get_record('user_info_field', ['shortname' => $fieldname]); return ($record) ? $record : (object) []; } /** * This reports back if the category has been deleted or the config value is different. * * @param int $categoryid The category id to check against. * @return bool True is the category checks out, otherwise false. */ private static function check_profile_category(int $categoryid = null): bool { global $DB; $categoryname = self::get_category_name(); $categorydata = $DB->get_record('user_info_category', ['name' => $categoryname]); if (empty($categorydata)) { return false; } if (isset($categoryid) && $categorydata->id != $categoryid) { return false; } return true; } /** * Are we using the proper user profile field to hold the mnet profile? * * @return bool True if we are using a user table field for the mnet profile. False means we are using costom profile fields. */ public static function official_profile_exists(): bool { global $DB; $usertablecolumns = $DB->get_columns('user', false); if (isset($usertablecolumns['moodlenetprofile'])) { return true; } return false; } /** * Gets the category name that is set for this site. * * @return string The category used to hold the moodle net profile field. */ public static function get_category_name(): string { return get_config('tool_moodlenet', 'profile_category'); } /** * Sets the a unique category to hold the moodle net user profile. * * @param string $categoryname The base category name to use. * @return string The actual name of the category to use. */ private static function set_category_name(string $categoryname): string { global $DB; $attemptname = $categoryname; // Check if this category already exists. $foundcategoryname = false; $i = 0; do { $category = $DB->count_records('user_info_category', ['name' => $attemptname]); if ($category > 0) { $i++; $attemptname = $categoryname . $i; } else { set_config('profile_category', $attemptname, 'tool_moodlenet'); $foundcategoryname = true; } } while (!$foundcategoryname); return $attemptname; } /** * Create a custom user profile category to hold our custom field. * * @return int The id of the created category. */ public static function create_user_profile_category(): int { global $DB; // No nice API to do this, so direct DB calls it is. $data = new \stdClass(); $data->sortorder = $DB->count_records('user_info_category') + 1; $data->name = self::set_category_name(get_string('pluginname', 'tool_moodlenet')); $data->id = $DB->insert_record('user_info_category', $data, true); $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id)); \core\event\user_info_category_created::create_from_category($createdcategory)->trigger(); return $createdcategory->id; } /** * Sets a unique name to be used for the moodle net profile. * * @param string $fieldname The base fieldname to use. * @return string The actual profile field name. */ private static function set_profile_field_name(string $fieldname): string { global $DB; $attemptname = $fieldname; // Check if this profilefield already exists. $foundfieldname = false; $i = 0; do { $profilefield = $DB->count_records('user_info_field', ['shortname' => $attemptname]); if ($profilefield > 0) { $i++; $attemptname = $fieldname . $i; } else { set_config('profile_field_name', $attemptname, 'tool_moodlenet'); $foundfieldname = true; } } while (!$foundfieldname); return $attemptname; } /** * Gets the unique profile field used to hold the moodle net profile. * * @return string The profile field name being used on this site. */ public static function get_profile_field_name(): string { return get_config('tool_moodlenet', 'profile_field_name'); } /** * Create a user profile field to hold the moodlenet profile information. * * @param int $categoryid The category to put this field into. */ public static function create_user_profile_text_field(int $categoryid): void { global $CFG; require_once($CFG->dirroot . '/user/profile/definelib.php'); require_once($CFG->dirroot . '/user/profile/field/text/define.class.php'); // Add our moodlenet profile field. $profileclass = new \profile_define_text(); $data = (object) [ 'shortname' => self::set_profile_field_name('mnetprofile'), 'name' => get_string('mnetprofile', 'tool_moodlenet'), 'datatype' => 'text', 'description' => get_string('mnetprofiledesc', 'tool_moodlenet'), 'descriptionformat' => 1, 'categoryid' => $categoryid, 'signup' => 1, 'forceunique' => 1, 'visible' => 2, 'param1' => 30, 'param2' => 2048 ]; $profileclass->define_save($data); } /** * Given our $moodlenetprofile let's cURL the domains' WebFinger endpoint * * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to get info from. * @return array [bool, text, raw] */ public static function get_moodlenet_profile_link(moodlenet_user_profile $moodlenetprofile): array { $domain = $moodlenetprofile->get_domain(); $username = $moodlenetprofile->get_username(); // Assumption: All MoodleNet instance's will contain a WebFinger validation script. $url = "https://".$domain."/.well-known/webfinger?resource=acct:".$username."@".$domain; $curl = new \curl(); $options = [ 'CURLOPT_HEADER' => 0, ]; $content = $curl->get($url, null, $options); $info = $curl->get_info(); // The base cURL seems fine, let's press on. if (!$curl->get_errno() && !$curl->error) { // WebFinger gave us a 404 back so the user has no droids here. if ($info['http_code'] >= 400) { if ($info['http_code'] === 404) { // User not found. return [ 'result' => false, 'message' => get_string('profilevalidationfail', 'tool_moodlenet'), ]; } else { // There was some other error that was not a missing account. return [ 'result' => false, 'message' => get_string('profilevalidationerror', 'tool_moodlenet'), ]; } } // We must have a valid link so give it back to the user. $data = json_decode($content); return [ 'result' => true, 'message' => get_string('profilevalidationpass', 'tool_moodlenet'), 'domain' => $data->aliases[0] ]; } else { // There was some failure in curl so report it back. return [ 'result' => false, 'message' => get_string('profilevalidationerror', 'tool_moodlenet'), ]; } } } moodlenet_user_profile.php 0000644 00000006165 15151264516 0012037 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Moodle net user profile class. * * @package tool_moodlenet * @copyright 2020 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet; /** * A class to represent the moodlenet profile. * * @package tool_moodlenet * @copyright 2020 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class moodlenet_user_profile { /** @var string $profile The full profile name. */ protected $profile; /** @var int $userid The user ID that this profile belongs to. */ protected $userid; /** @var string $username The username from $userprofile */ protected $username; /** @var string $domain The domain from $domain */ protected $domain; /** * Constructor method. * * @param string $userprofile The moodle net user profile string. * @param int $userid The user ID that this profile belongs to. */ public function __construct(string $userprofile, int $userid) { $this->profile = $userprofile; $this->userid = $userid; $explodedprofile = explode('@', $this->profile); if (count($explodedprofile) === 2) { // It'll either be an email or WebFinger entry. $this->username = $explodedprofile[0]; $this->domain = $explodedprofile[1]; } else if (count($explodedprofile) === 3) { // We may have a profile link as MoodleNet gives to the user. $this->username = $explodedprofile[1]; $this->domain = $explodedprofile[2]; } else { throw new \moodle_exception('invalidmoodlenetprofile', 'tool_moodlenet'); } } /** * Get the full moodle net profile. * * @return string The moodle net profile. */ public function get_profile_name(): string { return $this->profile; } /** * Get the user ID that this profile belongs to. * * @return int The user ID. */ public function get_userid(): int { return $this->userid; } /** * Get the username for this profile. * * @return string The username. */ public function get_username(): string { return $this->username; } /** * Get the domain for this profile. * * @return string The domain. */ public function get_domain(): string { return $this->domain; } } task/send_mnet_profiles_data_removed_notification.php 0000644 00000003420 15151264516 0017363 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace tool_moodlenet\task; use core\message\message; /** * Ad-hoc task to send a notification to admin stating that the user data related to the linked MoodleNet profiles has * been removed. * * @package tool_moodlenet * @copyright 2022 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_mnet_profiles_data_removed_notification extends \core\task\adhoc_task { public function execute(): void { $message = new message(); $message->component = 'moodle'; $message->name = 'notices'; $message->userfrom = \core_user::get_noreply_user(); $message->userto = get_admin(); $message->notification = 1; $message->subject = get_string('removedmnetprofilenotification_subject', 'tool_moodlenet'); $message->fullmessageformat = FORMAT_HTML; $message->fullmessagehtml = get_string('removedmnetprofilenotification', 'tool_moodlenet'); $message->smallmessage = strip_tags($message->fullmessagehtml); message_send($message); } } task/post_install.php 0000644 00000002600 15151264516 0010736 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace tool_moodlenet\task; /** * Ad-hoc task to perform post install tasks. * We use this to set the active activity chooser footer plugin to tool_moodlenet. * We couldn't do this directly in install.php, because there is an admin_apply_default_settings() call after all plugins are * installed and that would reset whatever value we had set earlier to 'hidden'. * * @package tool_moodlenet * @copyright 2022 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class post_install extends \core\task\adhoc_task { public function execute() { set_config('activitychooseractivefooter', 'tool_moodlenet'); } } task/send_enable_notification.php 0000644 00000004066 15151264516 0013240 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace tool_moodlenet\task; /** * Ad-hoc task to send the notification to admin stating MoodleNet is automatically enabled after upgrade. * * @package tool_moodlenet * @copyright 2022 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_enable_notification extends \core\task\adhoc_task { public function execute(): void { $message = new \core\message\message(); $message->component = 'moodle'; $message->name = 'notices'; $message->userfrom = \core_user::get_noreply_user(); $message->userto = get_admin(); $message->notification = 1; $message->contexturl = (new \moodle_url('/admin/settings.php', ['section' => 'optionalsubsystems'], 'admin-enablemoodlenet'))->out(false); $message->contexturlname = get_string('advancedfeatures', 'admin'); $message->subject = get_string('autoenablenotification_subject', 'tool_moodlenet'); $message->fullmessageformat = FORMAT_HTML; $message->fullmessagehtml = get_string('autoenablenotification', 'tool_moodlenet', (object) [ 'settingslink' => (new \moodle_url('/admin/settings.php', ['section' => 'tool_moodlenet']))->out(false), ]); $message->smallmessage = strip_tags($message->fullmessagehtml); message_send($message); } } local/import_handler_info.php 0000644 00000006055 15151264516 0012405 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_handler_info class. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The import_handler_info class. * * An import_handler_info object represent an resource import handler for a particular module. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_handler_info { /** @var string $modulename the name of the module. */ protected $modulename; /** @var string $description the description. */ protected $description; /** @var import_strategy $importstrategy the strategy which will be used to import resources handled by this handler */ protected $importstrategy; /** * The import_handler_info constructor. * * @param string $modulename the name of the module handling the file extension. E.g. 'label'. * @param string $description A description of how the module handles files of this extension type. * @param import_strategy $strategy the strategy which will be used to import the resource. * @throws \coding_exception */ public function __construct(string $modulename, string $description, import_strategy $strategy) { if (empty($modulename)) { throw new \coding_exception("Module name cannot be empty."); } if (empty($description)) { throw new \coding_exception("Description cannot be empty."); } $this->modulename = $modulename; $this->description = $description; $this->importstrategy = $strategy; } /** * Get the name of the module. * * @return string the module name, e.g. 'label'. */ public function get_module_name(): string { return $this->modulename; } /** * Get a human readable, localised description of how the file is handled by the module. * * @return string the localised description. */ public function get_description(): string { return $this->description; } /** * Get the import strategy used by this handler. * * @return import_strategy the import strategy object. */ public function get_strategy(): import_strategy { return $this->importstrategy; } } local/import_processor.php 0000644 00000021616 15151264516 0011774 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_processor class. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The import_processor class. * * The import_processor objects provide a means to import a remote resource into a course section, delegating the handling of * content to the relevant module, via its dndupload_handler callback. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_processor { /** @var object The course that we are uploading to */ protected $course = null; /** @var int The section number we are uploading to */ protected $section = null; /** @var import_handler_registry $handlerregistry registry object to use for cross checking the supplied handler.*/ protected $handlerregistry; /** @var import_handler_info $handlerinfo information about the module handling the import.*/ protected $handlerinfo; /** @var \stdClass $user the user conducting the import.*/ protected $user; /** @var remote_resource $remoteresource the remote resource being imported.*/ protected $remoteresource; /** @var string[] $descriptionoverrides list of modules which support having their descriptions updated, post-import. */ protected $descriptionoverrides = ['folder', 'page', 'resource', 'scorm', 'url']; /** * The import_processor constructor. * * @param \stdClass $course the course object. * @param int $section the section number in the course, starting at 0. * @param remote_resource $remoteresource the remote resource to import. * @param import_handler_info $handlerinfo information about which module is handling the import. * @param import_handler_registry $handlerregistry A registry of import handlers, to use for validation. * @throws \coding_exception If any of the params are invalid. */ public function __construct(\stdClass $course, int $section, remote_resource $remoteresource, import_handler_info $handlerinfo, import_handler_registry $handlerregistry) { global $DB, $USER; if ($section < 0) { throw new \coding_exception("Invalid section number $section. Must be > 0."); } if (!$DB->record_exists('modules', array('name' => $handlerinfo->get_module_name()))) { throw new \coding_exception("Module {$handlerinfo->get_module_name()} does not exist"); } $this->course = $course; $this->section = $section; $this->handlerregistry = $handlerregistry; $this->user = $USER; $this->remoteresource = $remoteresource; $this->handlerinfo = $handlerinfo; // ALL handlers must have a strategy and ANY strategy can process ANY resource. // It is therefore NOT POSSIBLE to have a resource that CANNOT be processed by a handler. // So, there's no need to verify that the remote_resource CAN be handled by the handler. It always can. } /** * Run the import process, including file download, module creation and cleanup (cache purge, etc). */ public function process(): void { // Allow the strategy to do setup for this file import. $moduledata = $this->handlerinfo->get_strategy()->import($this->remoteresource, $this->user, $this->course, $this->section); // Create the course module, and add that information to the data to be sent to the plugin handling the resource. $cmdata = $this->create_course_module($this->course, $this->section, $this->handlerinfo->get_module_name()); $moduledata->coursemodule = $cmdata->id; // Now, send the data to the handling plugin to let it set up. $instanceid = plugin_callback('mod', $this->handlerinfo->get_module_name(), 'dndupload', 'handle', [$moduledata], 'invalidfunction'); if ($instanceid == 'invalidfunction') { $name = $this->handlerinfo->get_module_name(); throw new \coding_exception("$name does not support drag and drop upload (missing {$name}_dndupload_handle function)"); } // Now, update the module description if the module supports it and only if it's not currently set. $this->update_module_description($instanceid); // Finish setting up the course module. $this->finish_setup_course_module($instanceid, $cmdata->id); } /** * Update the module's description (intro), if that feature is supported. * * @param int $instanceid the instance id of the module to update. */ protected function update_module_description(int $instanceid): void { global $DB, $CFG; require_once($CFG->libdir . '/moodlelib.php'); if (plugin_supports('mod', $this->handlerinfo->get_module_name(), FEATURE_MOD_INTRO, true)) { require_once($CFG->libdir . '/editorlib.php'); require_once($CFG->libdir . '/modinfolib.php'); $rec = $DB->get_record($this->handlerinfo->get_module_name(), ['id' => $instanceid]); if (empty($rec->intro) || in_array($this->handlerinfo->get_module_name(), $this->descriptionoverrides)) { $updatedata = (object)[ 'id' => $instanceid, 'intro' => clean_param($this->remoteresource->get_description(), PARAM_TEXT), 'introformat' => editors_get_preferred_format() ]; $DB->update_record($this->handlerinfo->get_module_name(), $updatedata); rebuild_course_cache($this->course->id, true); } } } /** * Create the course module to hold the file/content that has been uploaded. * @param \stdClass $course the course object. * @param int $section the section. * @param string $modname the name of the module, e.g. 'label'. * @return \stdClass the course module data. */ protected function create_course_module(\stdClass $course, int $section, string $modname): \stdClass { global $CFG; require_once($CFG->dirroot . '/course/modlib.php'); list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $modname, $section); $data->visible = false; // The module is created in a hidden state. $data->coursemodule = $data->id = add_course_module($data); return $data; } /** * Finish off any course module setup, such as adding to the course section and firing events. * * @param int $instanceid id returned by the mod when it was created. * @param int $cmid the course module record id, for removal if something went wrong. */ protected function finish_setup_course_module($instanceid, int $cmid): void { global $DB; if (!$instanceid) { // Something has gone wrong - undo everything we can. course_delete_module($cmid); throw new \moodle_exception('errorcreatingactivity', 'moodle', '', $this->handlerinfo->get_module_name()); } // Note the section visibility. $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible; $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $cmid)); // Rebuild the course cache after update action. rebuild_course_cache($this->course->id, true); course_add_cm_to_section($this->course, $cmid, $this->section); set_coursemodule_visible($cmid, $visible); if (!$visible) { $DB->set_field('course_modules', 'visibleold', 1, array('id' => $cmid)); } // Retrieve the final info about this module. $info = get_fast_modinfo($this->course, $this->user->id); if (!isset($info->cms[$cmid])) { // The course module has not been properly created in the course - undo everything. course_delete_module($cmid); throw new \moodle_exception('errorcreatingactivity', 'moodle', '', $this->handlerinfo->get_module_name()); } $mod = $info->get_cm($cmid); // Trigger course module created event. $event = \core\event\course_module_created::create_from_cm($mod); $event->trigger(); } } local/import_strategy_file.php 0000644 00000016515 15151264516 0012620 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_strategy_file class. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; use core\antivirus\manager as avmanager; /** * The import_strategy_file class. * * The import_strategy_file objects contains the setup steps needed to prepare a resource for import as a file into Moodle. This * ensures the remote_resource is first downloaded and put in a draft file area, ready for use as a file by the handling module. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_strategy_file implements import_strategy { /** * Get an array of import_handler_info objects representing modules supporting import of this file type. * * @param array $registrydata the fully populated registry. * @param remote_resource $resource the remote resource. * @return import_handler_info[] the array of import_handler_info objects. */ public function get_handlers(array $registrydata, remote_resource $resource): array { $handlers = []; foreach ($registrydata['files'] as $index => $items) { foreach ($items as $item) { if ($index === $resource->get_extension() || $index === '*') { $handlers[] = new import_handler_info($item['module'], $item['message'], $this); } } } return $handlers; } /** * Import the remote resource according to the rules of this strategy. * * @param remote_resource $resource the resource to import. * @param \stdClass $user the user to import on behalf of. * @param \stdClass $course the course into which the remote_resource is being imported. * @param int $section the section into which the remote_resource is being imported. * @return \stdClass the module data. * @throws \moodle_exception if the file size means the upload limit is exceeded for the user. */ public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass { // Before starting a potentially lengthy download, try to ensure the file size does not exceed the upload size restrictions // for the user. This is a time saving measure. // This is a naive check, that serves only to catch files if they provide the content length header. // Because of potential content encoding (compression), the stored file will be checked again after download as well. $size = $resource->get_download_size() ?? -1; $useruploadlimit = $this->get_user_upload_limit($user, $course); if ($this->size_exceeds_upload_limit($size, $useruploadlimit)) { throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $size, 'uploadlimit' => $useruploadlimit]); } // Download the file into a request directory and scan it. [$filepath, $filename] = $resource->download_to_requestdir(); avmanager::scan_file($filepath, $filename, true); // Check the final size of file against the user upload limits. $localsize = filesize(sprintf('%s/%s', $filepath, $filename)); if ($this->size_exceeds_upload_limit($localsize, $useruploadlimit)) { throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $localsize, 'uploadlimit' => $useruploadlimit]); } // Store in the user draft file area. $storedfile = $this->create_user_draft_stored_file($user, $filename, $filepath); // Prepare the data to be sent to the modules dndupload_handle hook. return $this->prepare_module_data($course, $resource, $storedfile->get_itemid()); } /** * Creates the data to pass to the dndupload_handle() hooks. * * @param \stdClass $course the course record. * @param remote_resource $resource the resource being imported as a file. * @param int $draftitemid the itemid of the draft file. * @return \stdClass the data object. */ protected function prepare_module_data(\stdClass $course, remote_resource $resource, int $draftitemid): \stdClass { $data = new \stdClass(); $data->type = 'Files'; $data->course = $course; $data->draftitemid = $draftitemid; $data->displayname = $resource->get_name(); return $data; } /** * Get the max file size limit for the user in the course. * * @param \stdClass $user the user to check. * @param \stdClass $course the course to check in. * @return int the file size limit, in bytes. */ protected function get_user_upload_limit(\stdClass $user, \stdClass $course): int { return get_user_max_upload_file_size(\context_course::instance($course->id), get_config('core', 'maxbytes'), $course->maxbytes, 0, $user); } /** * Does the size exceed the upload limit for the current import, taking into account user and core settings. * * @param int $sizeinbytes the size, in bytes. * @param int $useruploadlimit the upload limit, in bytes. * @return bool true if exceeded, false otherwise. * @throws \dml_exception */ protected function size_exceeds_upload_limit(int $sizeinbytes, int $useruploadlimit): bool { if ($useruploadlimit != USER_CAN_IGNORE_FILE_SIZE_LIMITS && $sizeinbytes > $useruploadlimit) { return true; } return false; } /** * Create a file in the user drafts ready for use by plugins implementing dndupload_handle(). * * @param \stdClass $user the user object. * @param string $filename the name of the file on disk * @param string $path the path where the file is stored on disk * @return \stored_file */ protected function create_user_draft_stored_file(\stdClass $user, string $filename, string $path): \stored_file { global $CFG; $record = new \stdClass(); $record->filearea = 'draft'; $record->component = 'user'; $record->filepath = '/'; $record->itemid = file_get_unused_draft_itemid(); $record->license = $CFG->sitedefaultlicense; $record->author = ''; $record->filename = clean_param($filename, PARAM_FILE); $record->contextid = \context_user::instance($user->id)->id; $record->userid = $user->id; $fullpathwithname = sprintf('%s/%s', $path, $filename); $fs = get_file_storage(); return $fs->create_file_from_pathname($record, $fullpathwithname); } } local/import_info.php 0000644 00000010225 15151264516 0010702 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_info class. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * Class import_info, describing objects which represent a resource being imported by a user. * * Objects of this class encapsulate both: * - information about the resource (remote_resource). * - config data pertaining to the import process, such as the destination course and section * and how the resource should be treated (i.e. the type and the name of the module selected as the import handler) * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_info { /** @var int $userid the user conducting this import. */ protected $userid; /** @var remote_resource $resource the resource being imported. */ protected $resource; /** @var \stdClass $config config data pertaining to the import process, e.g. course, section, type. */ protected $config; /** @var string $id string identifier for this object. */ protected $id; /** * The import_controller constructor. * * @param int $userid the id of the user performing the import. * @param remote_resource $resource the resource being imported. * @param \stdClass $config import config like 'course', 'section', 'type'. */ public function __construct(int $userid, remote_resource $resource, \stdClass $config) { $this->userid = $userid; $this->resource = $resource; $this->config = $config; $this->id = md5($resource->get_url()->get_value()); } /** * Get the id of this object. */ public function get_id() { return $this->id; } /** * Get the remote resource being imported. * * @return remote_resource the remote resource being imported. */ public function get_resource(): remote_resource { return $this->resource; } /** * Get the configuration data pertaining to the import. * * @return \stdClass the import configuration data. */ public function get_config(): \stdClass { return $this->config; } /** * Set the configuration data pertaining to the import. * * @param \stdClass $config the configuration data to set. */ public function set_config(\stdClass $config): void { $this->config = $config; } /** * Get an import_info object by id. * * @param string $id the id of the import_info object to load. * @return mixed an import_info object if found, otherwise null. */ public static function load(string $id): ?import_info { // This currently lives in the session, so we don't need userid. // It might be useful if we ever move to another storage mechanism however, where we would need it. global $SESSION; return isset($SESSION->moodlenetimports[$id]) ? unserialize($SESSION->moodlenetimports[$id]) : null; } /** * Save this object to a store which is accessible across requests. */ public function save(): void { global $SESSION; $SESSION->moodlenetimports[$this->id] = serialize($this); } /** * Remove all information about an import from the store. */ public function purge(): void { global $SESSION; unset($SESSION->moodlenetimports[$this->id]); } } local/import_backup_helper.php 0000644 00000017651 15151264516 0012565 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_backup_helper class. * * @package tool_moodlenet * @copyright 2020 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The import_backup_helper class. * * The import_backup_helper objects provide a means to prepare a backup for for restoration of a course or activity backup file. * * @copyright 2020 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_backup_helper { /** @var remote_resource $remoteresource A file resource to be restored. */ protected $remoteresource; /** @var user $user The user trying to restore a file. */ protected $user; /** @var context $context The context we are trying to restore this file into. */ protected $context; /** @var int $useruploadlimit The size limit that this user can upload in this context. */ protected $useruploadlimit; /** * Constructor for the import backup helper. * * @param remote_resource $remoteresource A remote file resource * @param \stdClass $user The user importing a file. * @param \context $context Context to restore into. */ public function __construct(remote_resource $remoteresource, \stdClass $user, \context $context) { $this->remoteresource = $remoteresource; $this->user = $user; $this->context = $context; $maxbytes = 0; if ($this->context->contextlevel == CONTEXT_COURSE) { $course = get_course($this->context->instanceid); $maxbytes = $course->maxbytes; } $this->useruploadlimit = get_user_max_upload_file_size($this->context, get_config('core', 'maxbytes'), $maxbytes, 0, $this->user); } /** * Return a stored user draft file for processing. * * @return \stored_file The imported file to ultimately be restored. */ public function get_stored_file(): \stored_file { // Check if the user can upload a backup to this context. require_capability('moodle/restore:uploadfile', $this->context, $this->user->id); // Before starting a potentially lengthy download, try to ensure the file size does not exceed the upload size restrictions // for the user. This is a time saving measure. // This is a naive check, that serves only to catch files if they provide the content length header. // Because of potential content encoding (compression), the stored file will be checked again after download as well. $size = $this->remoteresource->get_download_size() ?? -1; if ($this->size_exceeds_upload_limit($size)) { throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $size, 'uploadlimit' => $this->useruploadlimit]); } [$filepath, $filename] = $this->remoteresource->download_to_requestdir(); \core\antivirus\manager::scan_file($filepath, $filename, true); // Check the final size of file against the user upload limits. $localsize = filesize(sprintf('%s/%s', $filepath, $filename)); if ($this->size_exceeds_upload_limit($localsize)) { throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $localsize, 'uploadlimit' => $this->useruploadlimit]); } return $this->create_user_draft_stored_file($filename, $filepath); } /** * Does the size exceed the upload limit for the current import, taking into account user and core settings. * * @param int $sizeinbytes * @return bool true if exceeded, false otherwise. */ protected function size_exceeds_upload_limit(int $sizeinbytes): bool { $maxbytes = 0; if ($this->context->contextlevel == CONTEXT_COURSE) { $course = get_course($this->context->instanceid); $maxbytes = $course->maxbytes; } $maxbytes = get_user_max_upload_file_size($this->context, get_config('core', 'maxbytes'), $maxbytes, 0, $this->user); if ($maxbytes != USER_CAN_IGNORE_FILE_SIZE_LIMITS && $sizeinbytes > $maxbytes) { return true; } return false; } /** * Create a file in the user drafts ready for use by plugins implementing dndupload_handle(). * * @param string $filename the name of the file on disk * @param string $path the path where the file is stored on disk * @return \stored_file */ protected function create_user_draft_stored_file(string $filename, string $path): \stored_file { global $CFG; $record = new \stdClass(); $record->filearea = 'draft'; $record->component = 'user'; $record->filepath = '/'; $record->itemid = file_get_unused_draft_itemid(); $record->license = $CFG->sitedefaultlicense; $record->author = ''; $record->filename = clean_param($filename, PARAM_FILE); $record->contextid = \context_user::instance($this->user->id)->id; $record->userid = $this->user->id; $fullpathwithname = sprintf('%s/%s', $path, $filename); $fs = get_file_storage(); return $fs->create_file_from_pathname($record, $fullpathwithname); } /** * Looks for a context that this user has permission to upload backup files to. * This gets a list of roles that the user has, checks for the restore:uploadfile capability and then sends back a context * that has this permission if available. * * This starts with the highest context level and moves down i.e. system -> category -> course. * * @param int $userid The user ID that we are looking for a working context for. * @return \context A context that allows the upload of backup files. */ public static function get_context_for_user(int $userid): ?\context { global $DB; if (is_siteadmin()) { return \context_system::instance(); } $sql = "SELECT ctx.id, ctx.contextlevel, ctx.instanceid, ctx.path, ctx.depth, ctx.locked FROM {context} ctx JOIN {role_assignments} r ON ctx.id = r.contextid WHERE r.userid = :userid AND ctx.contextlevel IN (:contextsystem, :contextcategory, :contextcourse) ORDER BY ctx.contextlevel ASC"; $params = [ 'userid' => $userid, 'contextsystem' => CONTEXT_SYSTEM, 'contextcategory' => CONTEXT_COURSECAT, 'contextcourse' => CONTEXT_COURSE ]; $records = $DB->get_records_sql($sql, $params); foreach ($records as $record) { \context_helper::preload_from_record($record); if ($record->contextlevel == CONTEXT_COURSECAT) { $context = \context_coursecat::instance($record->instanceid); } else if ($record->contextlevel == CONTEXT_COURSE) { $context = \context_course::instance($record->instanceid); } else { $context = \context_system::instance(); } if (has_capability('moodle/restore:uploadfile', $context, $userid)) { return $context; } } return null; } } local/url.php 0000644 00000005121 15151264516 0007156 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the url class, providing a representation of a url and operations on its component parts. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The url class, providing a representation of a url and operations on its component parts. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class url { /** @var string $url the full URL string.*/ protected $url; /** @var string|null $path the path component of this URL.*/ protected $path; /** @var host|null $host the host component of this URL.*/ protected $host; /** * The url constructor. * * @param string $url the URL string. * @throws \coding_exception if the URL does not pass syntax validation. */ public function __construct(string $url) { // This object supports URLs as per the spec, so non-ascii chars must be encoded as per IDNA rules. if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new \coding_exception('Malformed URL'); } $this->url = $url; $this->path = parse_url($url, PHP_URL_PATH); $this->host = parse_url($url, PHP_URL_HOST); } /** * Get the path component of the URL. * * @return string|null the path component of the URL. */ public function get_path(): ?string { return $this->path; } /** * Return the domain component of the URL. * * @return string|null the domain component of the URL. */ public function get_host(): ?string { return $this->host; } /** * Return the full URL string. * * @return string the full URL string. */ public function get_value() { return $this->url; } } local/import_strategy.php 0000644 00000007016 15151264516 0011615 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_strategy interface. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The import_strategy interface. * * This provides a contract allowing different import strategies to be implemented. * * An import_strategy encapsulates the logic used to prepare a remote_resource for import into Moodle in some way and is used by the * import_processor (to perform aforementioned preparations) before it hands control of the import over to a course module plugin. * * We may wish to have many strategies because the preparation steps may vary depending on how the resource is to be treated. * E.g. We may wish to import as a file in which case download steps will be required, or we may simply wish to import the remote * resource as a link, in which cases setup steps will not require any file download. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface import_strategy { /** * Get an array of import_handler_info objects supported by this import strategy, based on the registrydata and resource. * * Implementations should check the registry data for any entries which align with their import strategy and should create * import_handler_info objects to represent each relevant entry. If an entry represents a module, or handling type which does * not align with the strategy, that item should simply be skipped. * * E.g. If one strategy aims to import all remote resources as files (e.g. import_strategy_file), it would only generate a list * of import_handler_info objects created from those registry entries of type 'file', as those entries represent the modules * which have said they can handle resources as files. * * @param array $registrydata The fully populated handler registry. * @param remote_resource $resource the remote resource. * @return import_handler_info[] the array of import_handler_info objects, or an empty array if none were matched. */ public function get_handlers(array $registrydata, remote_resource $resource): array; /** * Called during import to perform required import setup steps. * * @param remote_resource $resource the resource to import. * @param \stdClass $user the user to import on behalf of. * @param \stdClass $course the course into which the remote resource is being imported. * @param int $section the section into which the remote resource is being imported. * @return \stdClass the module data which will be passed on to the course module plugin. */ public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass; } local/import_handler_registry.php 0000644 00000017045 15151264516 0013323 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_handler_registry class. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The import_handler_registry class. * * The import_handler_registry objects represent a register of modules handling various file extensions for a given course and user. * Only modules which are available to the user in the course are included in the register for that user. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_handler_registry { /** * @var array array containing the names and messages of all modules handling import of resources as a 'file' type. */ protected $filehandlers = []; /** * @var array $typehandlers the array of modules registering as handlers of other, non-file types, indexed by typename. */ protected $typehandlers = []; /** * @var array $registry the aggregate of all registrations made by plugins, indexed by 'file' and 'type'. */ protected $registry = []; /** * @var \context_course the course context object. */ protected $context; /** * @var \stdClass a course object. */ protected $course; /** * @var \stdClass a user object. */ protected $user; /** * The import_handler_registry constructor. * * @param \stdClass $course the course, which impacts available handlers. * @param \stdClass $user the user, which impacts available handlers. */ public function __construct(\stdClass $course, \stdClass $user) { $this->course = $course; $this->user = $user; $this->context = \context_course::instance($course->id); // Generate the full list of handlers for all extensions for this user and course. $this->populate_handlers(); } /** * Get all handlers for the remote resource, depending on the strategy being used to import the resource. * * @param remote_resource $resource the remote resource. * @param import_strategy $strategy an import_strategy instance. * @return import_handler_info[] the array of import_handler_info handlers. */ public function get_resource_handlers_for_strategy(remote_resource $resource, import_strategy $strategy): array { return $strategy->get_handlers($this->registry, $resource); } /** * Get a specific handler for the resource, belonging to a specific module and for a specific strategy. * * @param remote_resource $resource the remote resource. * @param string $modname the name of the module, e.g. 'label'. * @param import_strategy $strategy a string representing how to treat the resource. e.g. 'file', 'link'. * @return import_handler_info|null the import_handler_info object, if found, otherwise null. */ public function get_resource_handler_for_mod_and_strategy(remote_resource $resource, string $modname, import_strategy $strategy): ?import_handler_info { foreach ($strategy->get_handlers($this->registry, $resource) as $handler) { if ($handler->get_module_name() === $modname) { return $handler; } } return null; } /** * Build up a list of extension handlers by leveraging the dndupload_register callbacks. */ protected function populate_handlers() { // Generate a dndupload_handler object, just so we can call ->is_known_type() on the types being registered by plugins. // We must vet each type which is reported to be handled against the list of known, supported types. global $CFG; require_once($CFG->dirroot . '/course/dnduploadlib.php'); $dndhandlers = new \dndupload_handler($this->course); // Get the list of mods enabled at site level first. We need to cross check this. $pluginman = \core_plugin_manager::instance(); $sitemods = $pluginman->get_plugins_of_type('mod'); $sitedisabledmods = array_filter($sitemods, function(\core\plugininfo\mod $modplugininfo){ return !$modplugininfo->is_enabled(); }); $sitedisabledmods = array_map(function($modplugininfo) { return $modplugininfo->name; }, $sitedisabledmods); // Loop through all modules to find the registered handlers. $mods = get_plugin_list_with_function('mod', 'dndupload_register'); foreach ($mods as $component => $funcname) { list($modtype, $modname) = \core_component::normalize_component($component); if (!empty($sitedisabledmods) && array_key_exists($modname, $sitedisabledmods)) { continue; // Module is disabled at the site level. } if (!course_allowed_module($this->course, $modname, $this->user)) { continue; // User does not have permission to add this module to the course. } if (!$resp = component_callback($component, 'dndupload_register')) { continue; }; if (isset($resp['files'])) { foreach ($resp['files'] as $file) { $this->register_file_handler($file['extension'], $modname, $file['message']); } } if (isset($resp['types'])) { foreach ($resp['types'] as $type) { if (!$dndhandlers->is_known_type($type['identifier'])) { throw new \coding_exception("Trying to add handler for unknown type $type"); } $this->register_type_handler($type['identifier'], $modname, $type['message']); } } } $this->registry = [ 'files' => $this->filehandlers, 'types' => $this->typehandlers ]; } /** * Adds a type handler to the list. * * @param string $identifier the name of the type. * @param string $module the name of the module, e.g. 'label'. * @param string $message the message describing how the module handles the type. */ protected function register_type_handler(string $identifier, string $module, string $message) { $this->typehandlers[$identifier][] = ['module' => $module, 'message' => $message]; } /** * Adds a file extension handler to the list. * * @param string $extension the extension, e.g. 'png'. * @param string $module the name of the module handling this extension * @param string $message the message describing how the module handles the extension. */ protected function register_file_handler(string $extension, string $module, string $message) { $extension = strtolower($extension); $this->filehandlers[$extension][] = ['module' => $module, 'message' => $message]; } } local/import_strategy_link.php 0000644 00000005465 15151264516 0012640 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the import_strategy_link class. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The import_strategy_link class. * * The import_strategy_link objects contains the setup steps needed to prepare a resource for import as a URL into Moodle. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_strategy_link implements import_strategy { /** * Get an array of import_handler_info objects representing modules supporting import of the resource. * * @param array $registrydata the fully populated registry. * @param remote_resource $resource the remote resource. * @return import_handler_info[] the array of import_handler_info objects. */ public function get_handlers(array $registrydata, remote_resource $resource): array { $handlers = []; foreach ($registrydata['types'] as $identifier => $items) { foreach ($items as $item) { if ($identifier === 'url') { $handlers[] = new import_handler_info($item['module'], $item['message'], $this); } } } return $handlers; } /** * Import the remote resource according to the rules of this strategy. * * @param remote_resource $resource the resource to import. * @param \stdClass $user the user to import on behalf of. * @param \stdClass $course the course into which the remote_resource is being imported. * @param int $section the section into which the remote_resource is being imported. * @return \stdClass the module data. */ public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass { $data = new \stdClass(); $data->type = 'url'; $data->course = $course; $data->content = $resource->get_url()->get_value(); $data->displayname = $resource->get_name(); return $data; } } local/remote_resource.php 0000644 00000013077 15151264516 0011567 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the remote_resource class definition. * * @package tool_moodlenet * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_moodlenet\local; /** * The remote_resource class. * * Objects of type remote_resource provide a means of interacting with resources over HTTP. * * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class remote_resource { /** @var \curl $curl the curl http helper.*/ protected $curl; /** @var url $url the url to the remote resource.*/ protected $url; /** @var string $filename the name of this remote file.*/ protected $filename; /** @var string $extension the file extension of this remote file.*/ protected $extension; /** @var array $headinfo the array of information for the most recent HEAD request.*/ protected $headinfo = []; /** @var \stdClass $metadata information about the resource. */ protected $metadata; /** * The remote_resource constructor. * * @param \curl $curl a curl object for HTTP requests. * @param url $url the URL of the remote resource. * @param \stdClass $metadata resource metadata such as name, summary, license, etc. */ public function __construct(\curl $curl, url $url, \stdClass $metadata) { $this->curl = $curl; $this->url = $url; $this->filename = pathinfo($this->url->get_path() ?? '', PATHINFO_FILENAME); $this->extension = pathinfo($this->url->get_path() ?? '', PATHINFO_EXTENSION); $this->metadata = $metadata; } /** * Return the URL for this remote resource. * * @return url the url object. */ public function get_url(): url { return $this->url; } /** * Get the name of the file as taken from the metadata. */ public function get_name(): string { return $this->metadata->name ?? ''; } /** * Get the resource metadata. * * @return \stdClass the metadata. */ public function get_metadata(): \stdClass { return$this->metadata; } /** * Get the description of the resource as taken from the metadata. * * @return string */ public function get_description(): string { return $this->metadata->description ?? ''; } /** * Return the extension of the file, if found. * * @return string the extension of the file, if found. */ public function get_extension(): string { return $this->extension; } /** * Returns the file size of the remote file, in bytes, or null if it cannot be determined. * * @return int|null the content length, if able to be determined, otherwise null. */ public function get_download_size(): ?int { $this->get_resource_info(); return $this->headinfo['download_content_length'] ?? null; } /** * Download the remote resource to a local requestdir, returning the path and name of the resulting file. * * @return array an array containing filepath adn filename, e.g. [filepath, filename]. * @throws \moodle_exception if the file cannot be downloaded. */ public function download_to_requestdir(): array { $filename = sprintf('%s.%s', $this->filename, $this->get_extension()); $path = make_request_directory(); $fullpathwithname = sprintf('%s/%s', $path, $filename); // In future, use a timeout (download and/or connection) controlled by a tool_moodlenet setting. $downloadtimeout = 30; $result = $this->curl->download_one($this->url->get_value(), null, ['filepath' => $fullpathwithname, 'timeout' => $downloadtimeout]); if ($result !== true) { throw new \moodle_exception('errorduringdownload', 'tool_moodlenet', '', $result); } return [$path, $filename]; } /** * Fetches information about the remote resource via a HEAD request. * * @throws \coding_exception if any connection problems occur. */ protected function get_resource_info() { if (!empty($this->headinfo)) { return; } $options['CURLOPT_RETURNTRANSFER'] = 1; $options['CURLOPT_FOLLOWLOCATION'] = 1; $options['CURLOPT_MAXREDIRS'] = 5; $options['CURLOPT_FAILONERROR'] = 1; // We want to consider http error codes as errors to report, not just status codes. $this->curl->head($this->url->get_value(), $options); $errorno = $this->curl->get_errno(); $this->curl->resetopt(); if ($errorno !== 0) { $message = 'Problem during HEAD request for remote resource \''.$this->url->get_value().'\'. Curl Errno: ' . $errorno; throw new \coding_exception($message); } $this->headinfo = $this->curl->get_info(); } } output/action_bar.php 0000644 00000010772 15151264741 0010733 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace mod_wiki\output; use moodle_url; use templatable; use renderable; /** * Renderable class for the action bar elements in the wiki activity pages. * * @package mod_wiki * @copyright 2021 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_bar implements templatable, renderable { /** @var int $pageid The database module id. */ private $pageid; /** @var moodle_url $currenturl The URL of the current page. */ private $currenturl; /** @var bool $displayprint Whether to display print wiki button. */ private $displayprint; /** * The class constructor. * * @param int $pageid The wiki page id. * @param moodle_url $pageurl The URL of the current page. * @param bool $displayprint Whether to display print wiki button. */ public function __construct(int $pageid, moodle_url $pageurl, bool $displayprint = false) { $this->pageid = $pageid; $this->currenturl = $pageurl; $this->displayprint = $displayprint; } /** * Export the data for the mustache template. * * @param \renderer_base $output renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { $urlselect = $this->get_action_selector(); $data = [ 'urlselect' => $urlselect->export_for_template($output), ]; if ($this->displayprint) { $printlink = new moodle_url('/mod/wiki/prettyview.php', ['pageid' => $this->pageid]); $data['printbutton'] = \html_writer::link($printlink, get_string('print', 'mod_wiki'), ['class' => 'btn btn-secondary', 'target' => "_blank"]); } return $data; } /** * Returns the URL selector object. * * @return \url_select The URL select object. */ private function get_action_selector(): \url_select { global $PAGE; $menu = []; if (has_capability('mod/wiki:viewpage', $PAGE->context)) { $viewlink = new moodle_url('/mod/wiki/view.php', ['pageid' => $this->pageid]); $menu[$viewlink->out(false)] = get_string('view', 'mod_wiki'); } if (has_capability('mod/wiki:editpage', $PAGE->context)) { $editlink = new moodle_url('/mod/wiki/edit.php', ['pageid' => $this->pageid]); $menu[$editlink->out(false)] = get_string('edit', 'mod_wiki'); } if (has_capability('mod/wiki:viewcomment', $PAGE->context)) { $commentslink = new moodle_url('/mod/wiki/comments.php', ['pageid' => $this->pageid]); $menu[$commentslink->out(false)] = get_string('comments', 'mod_wiki'); } if (has_capability('mod/wiki:viewpage', $PAGE->context)) { $historylink = new moodle_url('/mod/wiki/history.php', ['pageid' => $this->pageid]); $menu[$historylink->out(false)] = get_string('history', 'mod_wiki'); } if (has_capability('mod/wiki:viewpage', $PAGE->context)) { $maplink = new moodle_url('/mod/wiki/map.php', ['pageid' => $this->pageid]); $menu[$maplink->out(false)] = get_string('map', 'mod_wiki'); } if (has_capability('mod/wiki:viewpage', $PAGE->context)) { $fileslink = new moodle_url('/mod/wiki/files.php', ['pageid' => $this->pageid]); $menu[$fileslink->out(false)] = get_string('files', 'mod_wiki'); } if (has_capability('mod/wiki:managewiki', $PAGE->context)) { $adminlink = new moodle_url('/mod/wiki/admin.php', ['pageid' => $this->pageid]); $menu[$adminlink->out(false)] = get_string('admin', 'mod_wiki'); } return new \url_select($menu, $this->currenturl->out(false), null, 'wikiactionselect'); } } event/page_version_restored.php 0000644 00000006506 15151264741 0013003 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki version restored event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki version restored event class. * * @property-read array $other { * Extra information about event. * * - int pageid: id wiki page. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_version_restored extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_versions'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventversionrestored', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' restored version '$this->objectid' for the page with id '{$this->other['pageid']}' " . "for the wiki with course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'restore', 'view.php?pageid=' . $this->other['pageid'], $this->other['pageid'], $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/viewversion.php', array('pageid' => $this->other['pageid'], 'versionid' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['pageid'])) { throw new \coding_exception('The pageid needs to be set in $other'); } } public static function get_objectid_mapping() { return array('db' => 'wiki_versions', 'restore' => 'wiki_version'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['pageid'] = array('db' => 'wiki_pages', 'restore' => 'wiki_page'); return $othermapped; } } event/comments_viewed.php 0000644 00000004536 15151264741 0011604 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki comments viewed event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki comments viewed event class. * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comments_viewed extends \core\event\comments_viewed { /** * Init method. * * @return void */ protected function init() { parent::init(); $this->data['objecttable'] = 'wiki_pages'; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the comments for the page with id '$this->objectid' for the wiki " . "with course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'comments', 'comments.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/comments.php', array('pageid' => $this->objectid)); } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } } event/page_diff_viewed.php 0000644 00000007062 15151264741 0011660 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki diff viewed event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki diff viewed event class. * * @property-read array $other { * Extra information about event. * * - int comparewith: version number to compare with. * - int compare: version number to compare. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_diff_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventdiffviewed', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the diff for the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'diff', 'diff.php?pageid=' . $this->objectid . '&comparewith=' . $this->other['comparewith'] . '&compare=' . $this->other['compare'], $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/diff.php', array( 'pageid' => $this->objectid, 'comparewith' => $this->other['comparewith'], 'compare' => $this->other['compare'] )); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['comparewith'])) { throw new \coding_exception('The \'comparewith\' value must be set in other.'); } if (!isset($this->other['compare'])) { throw new \coding_exception('The \'compare\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } public static function get_other_mapping() { // Nothing to map. return false; } } event/page_locks_deleted.php 0000644 00000005543 15151264741 0012210 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki page locks deleted (override locks) event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki page locks deleted (override locks) event class. * * @property-read array $other { * Extra information about event. * * - string section: (optional) section name. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_locks_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpagelocksdeleted', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted locks for the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'overridelocks', 'view.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/view.php', array('pageid' => $this->objectid)); } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } public static function get_other_mapping() { // Nothing to map. return false; } } event/page_version_viewed.php 0000644 00000006544 15151264741 0012441 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki version viewed event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki version viewed event class. * * @property-read array $other { * Extra information about event. * * - int versionid: id page version. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_version_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventversionviewed', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the version for the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'history', 'viewversion.php?pageid=' . $this->objectid . '&versionid=' . $this->other['versionid'], $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/viewversion.php', array('pageid' => $this->objectid, 'versionid' => $this->other['versionid'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['versionid'])) { throw new \coding_exception('The versionid need to be set in $other'); } } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['versionid'] = array('db' => 'wiki_versions', 'restore' => 'wiki_version'); return $othermapped; } } event/page_deleted.php 0000644 00000005615 15151264741 0011015 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki page deleted event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki page deleted event class. * * @property-read array $other { * Extra information about event. * * - int subwikiid: (optional) id subwiki. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpagedeleted', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'admin', 'admin.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/admin.php', array('pageid' => $this->objectid)); } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['subwikiid'] = array('db' => 'wiki_subwikis', 'restore' => 'wiki_subwiki'); return $othermapped; } } event/page_created.php 0000644 00000005064 15151264741 0011014 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki page created event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki page created event class. * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_created extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpagecreated', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' created the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'add page', 'view.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/view.php', array('pageid' => $this->objectid)); } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } } event/comment_deleted.php 0000644 00000003454 15151264741 0011542 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki comment deleted event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki comment deleted event class. * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_deleted extends \core\event\comment_deleted { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/comments.php', array('pageid' => $this->other['itemid'])); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted a comment with id '$this->objectid' on the page with id " . "'{$this->other['itemid']}' for the wiki with course module id '$this->contextinstanceid'."; } } event/page_version_deleted.php 0000644 00000006442 15151264741 0012561 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki page version deleted event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki page version deleted event class. * * @property-read array $other { * Extra information about event. * * - int pageid: id wiki page. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_version_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_versions'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpageversiondeleted', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted version '$this->objectid' for the page with id '{$this->other['pageid']}' " . "for the wiki with course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'admin', 'admin.php?pageid=' . $this->other['pageid'], $this->other['pageid'], $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/admin.php', array('pageid' => $this->other['pageid'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['pageid'])) { throw new \coding_exception('The \'pageid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'wiki_versions', 'restore' => 'wiki_version'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['pageid'] = array('db' => 'wiki_pages', 'restore' => 'wiki_page'); return $othermapped; } } event/page_history_viewed.php 0000644 00000005125 15151264741 0012447 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki history viewed event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki history viewed event class. * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_history_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventhistoryviewed', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the history for the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'history', 'history.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/history.php', array('pageid' => $this->objectid)); } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } } event/page_viewed.php 0000644 00000010321 15151264741 0010660 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki page viewed event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki page viewed event class. * * @property-read array $other { * Extra information about the event. * * - string title: (optional) the wiki title * - int wid: (optional) the wiki id * - int group: (optional) the group id * - string groupanduser: (optional) the groupid-userid * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpageviewed', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { if (!empty($this->other['wid'])) { return(array($this->courseid, 'wiki', 'view', 'view.php?wid=' . $this->data['other']['wid'] . '&title=' . $this->data['other']['title'], $this->data['other']['wid'], $this->contextinstanceid)); } else if (!empty($this->other['prettyview'])) { return(array($this->courseid, 'wiki', 'view', 'prettyview.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } else { return(array($this->courseid, 'wiki', 'view', 'view.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { if (!empty($this->data['other']['wid'])) { return new \moodle_url('/mod/wiki/view.php', array('wid' => $this->data['other']['wid'], 'title' => $this->data['other']['title'], 'uid' => $this->relateduserid, 'groupanduser' => $this->data['other']['groupanduser'], 'group' => $this->data['other']['group'] )); } else if (!empty($this->other['prettyview'])) { return new \moodle_url('/mod/wiki/prettyview.php', array('pageid' => $this->objectid)); } else { return new \moodle_url('/mod/wiki/view.php', array('pageid' => $this->objectid)); } } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['wid'] = array('db' => 'wiki', 'restore' => 'wiki'); $othermapped['group'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } event/comment_created.php 0000644 00000003452 15151264741 0011541 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki comment created event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki comment created event class. * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_created extends \core\event\comment_created { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/comments.php', array('pageid' => $this->other['itemid'])); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' added a comment with id '$this->objectid' on the page with id " . "'{$this->other['itemid']}' for the wiki with course module id '$this->contextinstanceid'."; } } event/page_map_viewed.php 0000644 00000006160 15151264741 0011523 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki map viewed event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki map viewed event class. * * @property-read array $other { * Extra information about event. * * - int option: map option viewed. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_map_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventmapviewed', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the wiki map for the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'map', 'map.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/map.php', array('pageid' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['option'])) { throw new \coding_exception('The \'option\' value must be set in other, even if 0.'); } } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } public static function get_other_mapping() { // Nothing to map. return false; } } event/page_updated.php 0000644 00000005445 15151264741 0011036 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The mod_wiki page updated event. * * @package mod_wiki * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_wiki page updated event class. * * @property-read array $other { * Extra information about event. * * - string newcontent: (optional) updated content. * } * * @package mod_wiki * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'wiki_pages'; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventpageupdated', 'mod_wiki'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' updated the page with id '$this->objectid' for the wiki with " . "course module id '$this->contextinstanceid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { return(array($this->courseid, 'wiki', 'edit', 'view.php?pageid=' . $this->objectid, $this->objectid, $this->contextinstanceid)); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/wiki/view.php', array('pageid' => $this->objectid)); } public static function get_objectid_mapping() { return array('db' => 'wiki_pages', 'restore' => 'wiki_page'); } public static function get_other_mapping() { // Nothing to map. return false; } } search/collaborative_page.php 0000644 00000017412 15151264741 0012357 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Search area for mod_wiki collaborative pages. * * @package mod_wiki * @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_wiki\search; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/wiki/locallib.php'); /** * Search area for mod_wiki collaborative pages. * * @package mod_wiki * @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class collaborative_page extends \core_search\base_mod { /** * @var array Cache of wiki records. */ protected $wikiscache = array(); /** * Returns a recordset with all required page information. * * @param int $modifiedfrom * @param \context|null $context Optional context to restrict scope of returned results * @return moodle_recordset|null Recordset (or null if no results) */ public function get_document_recordset($modifiedfrom = 0, \context $context = null) { global $DB; list ($contextjoin, $contextparams) = $this->get_context_restriction_sql( $context, 'wiki', 'w'); if ($contextjoin === null) { return null; } $sql = "SELECT p.*, w.id AS wikiid, w.course AS courseid, s.groupid AS groupid FROM {wiki_pages} p JOIN {wiki_subwikis} s ON s.id = p.subwikiid JOIN {wiki} w ON w.id = s.wikiid $contextjoin WHERE p.timemodified >= ? AND w.wikimode = ? ORDER BY p.timemodified ASC"; return $DB->get_recordset_sql($sql, array_merge($contextparams, [$modifiedfrom, 'collaborative'])); } /** * Returns the document for a particular page. * * @param \stdClass $record A record containing, at least, the indexed document id and a modified timestamp * @param array $options Options for document creation * @return \core_search\document */ public function get_document($record, $options = array()) { try { $cm = $this->get_cm('wiki', $record->wikiid, $record->courseid); $context = \context_module::instance($cm->id); } catch (\dml_missing_record_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } catch (\dml_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } // Make a page object without extra fields. $page = clone $record; unset($page->courseid); unset($page->wikiid); // Conversion based wiki_print_page_content(). // Check if we have passed the cache time. if ($page->timerendered + WIKI_REFRESH_CACHE_TIME < time()) { $content = wiki_refresh_cachedcontent($page); $page = $content['page']; } // Convert to text. $content = content_to_text($page->cachedcontent, FORMAT_MOODLE); // Prepare associative array with data from DB. $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname); $doc->set('title', content_to_text($record->title, false)); $doc->set('content', $content); $doc->set('contextid', $context->id); $doc->set('courseid', $record->courseid); if ($record->groupid > 0) { $doc->set('groupid', $record->groupid); } $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $record->timemodified); // Check if this document should be considered new. if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $record->timecreated)) { // If the document was created after the last index time, it must be new. $doc->set_is_new(true); } return $doc; } /** * Can the current user see the document. * * @param int $id The internal search area entity id. * @return bool True if the user can see it, false otherwise */ public function check_access($id) { global $DB; try { $page = $DB->get_record('wiki_pages', array('id' => $id), '*', MUST_EXIST); if (!isset($this->wikiscache[$page->subwikiid])) { $sql = 'SELECT w.* FROM {wiki_subwikis} s JOIN {wiki} w ON w.id = s.wikiid WHERE s.id = ?'; $this->wikiscache[$page->subwikiid] = $DB->get_record_sql($sql, array('id' => $page->subwikiid), MUST_EXIST); } $wiki = $this->wikiscache[$page->subwikiid]; $cminfo = $this->get_cm('wiki', $wiki->id, $wiki->course); } catch (\dml_missing_record_exception $ex) { return \core_search\manager::ACCESS_DELETED; } catch (\dml_exception $ex) { return \core_search\manager::ACCESS_DENIED; } // Recheck uservisible although it should have already been checked in core_search. if ($cminfo->uservisible === false) { return \core_search\manager::ACCESS_DENIED; } $context = \context_module::instance($cminfo->id); if (!has_capability('mod/wiki:viewpage', $context)) { return \core_search\manager::ACCESS_DENIED; } return \core_search\manager::ACCESS_GRANTED; } /** * Returns a url to the page. * * @param \core_search\document $doc * @return \moodle_url */ public function get_doc_url(\core_search\document $doc) { $params = array('pageid' => $doc->get('itemid')); return new \moodle_url('/mod/wiki/view.php', $params); } /** * Returns a url to the wiki. * * @param \core_search\document $doc * @return \moodle_url */ public function get_context_url(\core_search\document $doc) { $contextmodule = \context::instance_by_id($doc->get('contextid')); return new \moodle_url('/mod/wiki/view.php', array('id' => $contextmodule->instanceid)); } /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } /** * Return the context info required to index files for * this search area. * * @return array */ public function get_search_fileareas() { $fileareas = array('attachments'); // Filearea. return $fileareas; } /** * Confirms that data entries support group restrictions. * * @return bool True */ public function supports_group_restriction() { return true; } } output/main.php 0000644 00000015151 15151265017 0007547 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for timeline block. * * @package block_timeline * @copyright 2018 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace block_timeline\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; use core_course\external\course_summary_exporter; require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . '/blocks/timeline/lib.php'); require_once($CFG->libdir . '/completionlib.php'); /** * Class containing data for timeline block. * * @copyright 2018 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class main implements renderable, templatable { /** Number of courses to load per page */ const COURSES_PER_PAGE = 2; /** * @var string The current filter preference */ public $filter; /** * @var string The current sort/order preference */ public $order; /** * @var string The current limit preference */ public $limit; /** @var int Number of timeline instances displayed. */ protected static $timelineinstances = 0; /** @var int This timeline instance's ID. */ protected $timelineinstanceid = 0; /** * main constructor. * * @param string $order Constant sort value from ../timeline/lib.php * @param string $filter Constant filter value from ../timeline/lib.php * @param string $limit Constant limit value from ../timeline/lib.php */ public function __construct($order, $filter, $limit) { $this->order = $order ? $order : BLOCK_TIMELINE_SORT_BY_DATES; $this->filter = $filter ? $filter : BLOCK_TIMELINE_FILTER_BY_7_DAYS; $this->limit = $limit ? $limit : BLOCK_TIMELINE_ACTIVITIES_LIMIT_DEFAULT; // Increment the timeline instances count on initialisation. self::$timelineinstances++; // Assign this instance an ID based on the latest timeline instances count. $this->timelineinstanceid = self::$timelineinstances; } /** * Test the available filters with the current user preference and return an array with * bool flags corresponding to which is active * * @return array */ protected function get_filters_as_booleans() { $filters = [ BLOCK_TIMELINE_FILTER_BY_NONE => false, BLOCK_TIMELINE_FILTER_BY_OVERDUE => false, BLOCK_TIMELINE_FILTER_BY_7_DAYS => false, BLOCK_TIMELINE_FILTER_BY_30_DAYS => false, BLOCK_TIMELINE_FILTER_BY_3_MONTHS => false, BLOCK_TIMELINE_FILTER_BY_6_MONTHS => false ]; // Set the selected filter to true. $filters[$this->filter] = true; return $filters; } /** * Get the offset/limit values corresponding to $this->filter * which are used to send through to the context as default values * * @return array */ private function get_filter_offsets() { $limit = ''; if (in_array($this->filter, [BLOCK_TIMELINE_FILTER_BY_NONE, BLOCK_TIMELINE_FILTER_BY_OVERDUE])) { $offset = -14; if ($this->filter == BLOCK_TIMELINE_FILTER_BY_OVERDUE) { $limit = 1; } } else { $offset = 0; $limit = 7; switch($this->filter) { case BLOCK_TIMELINE_FILTER_BY_30_DAYS: $limit = 30; break; case BLOCK_TIMELINE_FILTER_BY_3_MONTHS: $limit = 90; break; case BLOCK_TIMELINE_FILTER_BY_6_MONTHS: $limit = 180; break; } } return [ 'daysoffset' => $offset, 'dayslimit' => $limit ]; } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { $nocoursesurl = $output->image_url('courses', 'block_timeline')->out(); $noeventsurl = $output->image_url('activities', 'block_timeline')->out(); $requiredproperties = course_summary_exporter::define_properties(); $fields = join(',', array_keys($requiredproperties)); $courses = course_get_enrolled_courses_for_logged_in_user(0, 0, null, $fields); list($inprogresscourses, $processedcount) = course_filter_courses_by_timeline_classification( $courses, COURSE_TIMELINE_INPROGRESS, self::COURSES_PER_PAGE ); $formattedcourses = array_map(function($course) use ($output) { \context_helper::preload_from_record($course); $context = \context_course::instance($course->id); $exporter = new course_summary_exporter($course, ['context' => $context]); return $exporter->export($output); }, $inprogresscourses); $filters = $this->get_filters_as_booleans(); $offsets = $this->get_filter_offsets(); $contextvariables = [ 'timelineinstanceid' => $this->timelineinstanceid, 'midnight' => usergetmidnight(time()), 'coursepages' => [$formattedcourses], 'urls' => [ 'nocourses' => $nocoursesurl, 'noevents' => $noeventsurl ], 'sorttimelinedates' => $this->order == BLOCK_TIMELINE_SORT_BY_DATES, 'sorttimelinecourses' => $this->order == BLOCK_TIMELINE_SORT_BY_COURSES, 'selectedfilter' => $this->filter, 'hasdaysoffset' => true, 'hasdayslimit' => $offsets['dayslimit'] !== '' , 'nodayslimit' => $offsets['dayslimit'] === '' , 'limit' => $this->limit, 'hascourses' => !empty($formattedcourses), ]; return array_merge($contextvariables, $filters, $offsets); } } navigation/views/secondary.php 0000644 00000002027 15151765225 0012552 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_block\navigation\views; /** * Class secondary * * @package core_block * @category navigation */ class secondary extends \core\navigation\views\secondary { /** * Blocks don't require secondary navs as they can be accessed from multiple places and in different contexts. */ public function initialise(): void { } } external/fetch_addable_blocks.php 0000644 00000010537 15151765225 0013200 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This is the external method used for fetching the addable blocks in a given page. * * @package core_block * @since Moodle 3.11 * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_block\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/externallib.php'); use external_api; use external_function_parameters; use external_multiple_structure; use external_single_structure; use external_value; /** * This is the external method used for fetching the addable blocks in a given page. * * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fetch_addable_blocks extends external_api { /** * Describes the parameters for execute. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'pagecontextid' => new external_value(PARAM_INT, 'The context ID of the page.'), 'pagetype' => new external_value(PARAM_ALPHANUMEXT, 'The type of the page.'), 'pagelayout' => new external_value(PARAM_ALPHA, 'The layout of the page.'), 'subpage' => new external_value(PARAM_TEXT, 'The subpage identifier', VALUE_DEFAULT, ''), ] ); } /** * Fetch the addable blocks in a given page. * * @param int $pagecontextid The context ID of the page * @param string $pagetype The type of the page * @param string $pagelayout The layout of the page * @param string $subpage The subpage identifier * @return array The blocks list */ public static function execute(int $pagecontextid, string $pagetype, string $pagelayout, string $subpage = ''): array { global $PAGE; $params = self::validate_parameters(self::execute_parameters(), [ 'pagecontextid' => $pagecontextid, 'pagetype' => $pagetype, 'pagelayout' => $pagelayout, 'subpage' => $subpage, ] ); $context = \context::instance_by_id($params['pagecontextid']); // Validate the context. This will also set the context in $PAGE. self::validate_context($context); // We need to manually set the page layout and page type. $PAGE->set_pagelayout($params['pagelayout']); $PAGE->set_pagetype($params['pagetype']); $PAGE->set_subpage($params['subpage']); // Firstly, we need to load all currently existing page blocks to later determine which blocks are addable. $PAGE->blocks->load_blocks(false); $PAGE->blocks->create_all_block_instances(); $addableblocks = $PAGE->blocks->get_addable_blocks(); return array_map(function($block) { return [ 'name' => $block->name, 'title' => get_string('pluginname', "block_{$block->name}") ]; }, $addableblocks); } /** * Describes the execute return value. * * @return external_multiple_structure */ public static function execute_returns(): external_multiple_structure { return new external_multiple_structure( new external_single_structure( [ 'name' => new external_value(PARAM_PLUGIN, 'The name of the block.'), 'title' => new external_value(PARAM_RAW, 'The title of the block.'), ] ), 'List of addable blocks in a given page.' ); } } related_competency.php 0000644 00000016347 15151776373 0011155 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing related competencies from the DB. * * @package core_competency * @copyright 2015 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use lang_string; use stdClass; /** * Class for loading/storing related_competencies from the DB. * * @package core_competency * @copyright 2015 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class related_competency extends persistent { const TABLE = 'competency_relatedcomp'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'competencyid' => array( 'type' => PARAM_INT ), 'relatedcompetencyid' => array( 'type' => PARAM_INT ), ); } /** * Validate competency ID. * * @param int $data The competency ID. * @return true|lang_string */ protected function validate_competencyid($data) { if (!competency::record_exists($data)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate related competency ID. * * @param int $data The related competency ID. * @return true|lang_string */ protected function validate_relatedcompetencyid($data) { if ($this->get('competencyid') == $data) { // A competency cannot be related to itself. return new lang_string('invaliddata', 'error'); } if ($this->get('competencyid') > $data) { // The competency ID must be lower than the related competency ID. return new lang_string('invaliddata', 'error'); } else if (!competency::record_exists($data)) { return new lang_string('invaliddata', 'error'); } else if (!competency::share_same_framework(array($data, $this->get('competencyid')))) { // The competencies must belong to the same framework. return new lang_string('invaliddata', 'error'); } return true; } /** * Get relation specifying both competencies. * * This does not perform any validation on the data passed. If the relation exists in the database * then it is loaded in a the model, if not then it is up to the developer to save the model. * * @param int $competencyid * @param int $relatedcompetencyid * @return related_competency */ public static function get_relation($competencyid, $relatedcompetencyid) { global $DB; // Lower id always as competencyid so we know which one is competencyid and which one relatedcompetencyid. $relation = new static(); if ($competencyid > $relatedcompetencyid) { $relation->set('competencyid', $relatedcompetencyid); $relation->set('relatedcompetencyid', $competencyid); } else { $relation->set('competencyid', $competencyid); $relation->set('relatedcompetencyid', $relatedcompetencyid); } // We can do it because we have bidirectional relations in the DB. $params = array( 'competencyid' => $relation->get('competencyid'), 'relatedcompetencyid' => $relation->get('relatedcompetencyid') ); if ($record = $DB->get_record(self::TABLE, $params)) { $relation->from_record($record); } return $relation; } /** * Get the competencies related to a competency. * * @param int $competencyid The competency ID. * @return competency[] */ public static function get_related_competencies($competencyid) { global $DB; $fields = competency::get_sql_fields('c', 'c_'); $sql = "(SELECT $fields, " . $DB->sql_concat('rc.relatedcompetencyid', "'_'", 'rc.competencyid') . " AS rid FROM {" . self::TABLE . "} rc JOIN {" . competency::TABLE . "} c ON c.id = rc.relatedcompetencyid WHERE rc.competencyid = :cid) UNION ALL (SELECT $fields, " . $DB->sql_concat('rc.competencyid', "'_'", 'rc.relatedcompetencyid') . " AS rid FROM {" . self::TABLE . "} rc JOIN {" . competency::TABLE . "} c ON c.id = rc.competencyid WHERE rc.relatedcompetencyid = :cid2) ORDER BY c_path ASC, c_sortorder ASC"; $competencies = array(); $records = $DB->get_recordset_sql($sql, array('cid' => $competencyid, 'cid2' => $competencyid)); foreach ($records as $record) { unset($record->rid); $competencies[$record->c_id] = new competency(null, competency::extract_record($record, 'c_')); } $records->close(); return $competencies; } /** * Get the related competencies from competency ids. * * @param int[] $competencyids Array of competency ids. * @return related_competency[] */ public static function get_multiple_relations($competencyids) { global $DB; if (empty($competencyids)) { return array(); } list($insql, $params) = $DB->get_in_or_equal($competencyids); $records = $DB->get_records_select(self::TABLE, "competencyid $insql OR relatedcompetencyid $insql", array_merge($params, $params) ); $relatedcompetencies = array(); foreach ($records as $record) { unset($record->id); $relatedcompetencies[] = new related_competency(null, $record); } return $relatedcompetencies; } /** * Delete relations using competencies. * * @param array $competencyids Array of competencies ids. * @return bool True if relations were deleted successfully. */ public static function delete_multiple_relations($competencyids) { global $DB; if (empty($competencyids)) { return true; } list($insql, $params) = $DB->get_in_or_equal($competencyids); return $DB->delete_records_select(self::TABLE, "competencyid $insql OR relatedcompetencyid $insql", array_merge($params, $params) ); } } evidence.php 0000644 00000024443 15151776373 0007065 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Evidence persistent file. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use coding_exception; use context; use context_user; use lang_string; use moodle_exception; use stdClass; /** * Evidence persistent class. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class evidence extends persistent { const TABLE = 'competency_evidence'; /** Action logging. */ const ACTION_LOG = 0; /** Action rating a competency when no rating is set. */ const ACTION_COMPLETE = 2; /** Action rating a competency. */ const ACTION_OVERRIDE = 3; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'usercompetencyid' => array( 'type' => PARAM_INT ), 'contextid' => array( 'type' => PARAM_INT ), 'action' => array( 'type' => PARAM_INT, 'choices' => array(self::ACTION_LOG, self::ACTION_COMPLETE, self::ACTION_OVERRIDE) ), 'actionuserid' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED ), 'descidentifier' => array( 'type' => PARAM_STRINGID ), 'desccomponent' => array( 'type' => PARAM_COMPONENT ), 'desca' => array( 'type' => PARAM_RAW, 'default' => null, 'null' => NULL_ALLOWED ), 'url' => array( 'type' => PARAM_URL, 'default' => null, 'null' => NULL_ALLOWED ), 'grade' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED ), 'note' => array( 'type' => PARAM_NOTAGS, 'default' => null, 'null' => NULL_ALLOWED ) ); } /** * Return the competency linked to this. * * @return competency */ public function get_competency() { return user_competency::get_competency_by_usercompetencyid($this->get('usercompetencyid')); } /** * Return the evidence's context. * * @return context */ public function get_context() { return context::instance_by_id($this->get('contextid')); } /** * Convenience method to get the description $a. * * @return mixed */ protected function get_desca() { $value = $this->raw_get('desca'); if ($value !== null) { $value = json_decode($value); } return $value; } /** * Convenience method to get the description. * * @return lang_string */ public function get_description() { return new lang_string($this->get('descidentifier'), $this->get('desccomponent'), $this->get_desca()); } /** * Convenience method to set the description $a. * * @param mixed $value * @return mixed */ protected function set_desca($value) { if ($value !== null) { if (!is_scalar($value) && !is_array($value) && !($value instanceof stdClass)) { throw new coding_exception('$a format not supported.'); } $value = json_encode($value); } $this->raw_set('desca', $value); } /** * Convenience method handling moodle_urls. * * @param null|string|moodle_url $url The URL. */ protected function set_url($url) { if ($url instanceof \moodle_url) { $url = $url->out(false); } $this->raw_set('url', $url); } /** * Validate the action user ID. * * @param int $value A user ID. * @return true|lang_string */ protected function validate_actionuserid($value) { if ($value !== null && !\core_user::is_real_user($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the context ID. * * @param int $value * @return true|lang_string */ protected function validate_contextid($value) { try { context::instance_by_id($value); } catch (moodle_exception $e) { // That does not look good... return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the description $a. * * @param string $value * @return true|lang_string */ protected function validate_desca($value) { if ($value === null) { return true; } $desc = json_decode($value); if ($desc === null && json_last_error() !== JSON_ERROR_NONE) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the description identifier. * * Only validate string existence during create. If the string is removed later on we should * not prevent this model from being updated. Alternatively we could check if the string has * changed before performing the check but this overhead is not required for now. * An evidence should usually never be updated anyway. * * @param string $value * @return true|lang_string */ protected function validate_descidentifier($value) { if (!$this->get('id') && !get_string_manager()->string_exists($value, $this->get('desccomponent'))) { return new lang_string('invalidevidencedesc', 'core_competency'); } return true; } /** * Validate the grade. * * For performance reason we do not validate that the grade is a valid item of the * scale associated with the competency or framework. * * @param int $value The value. * @return true|lang_string */ protected function validate_grade($value) { if ($value !== null && $value <= 0) { return new lang_string('invalidgrade', 'core_competency'); } $action = $this->get('action'); if ($value === null && $action == self::ACTION_COMPLETE) { return new lang_string('invalidgrade', 'core_competency'); } else if ($value !== null && $action == self::ACTION_LOG) { return new lang_string('invalidgrade', 'core_competency'); } if ($value !== null) { // TODO MDL-52243 Use a core method to validate the grade_scale item. // Check if grade exist in the scale item values. $competency = $this->get_competency(); if (!array_key_exists($value - 1, $competency->get_scale()->scale_items)) { return new lang_string('invalidgrade', 'core_competency'); } } return true; } /** * Validate the user competency. * * @param int $value * @return true|lang_string */ protected function validate_usercompetencyid($value) { if (!user_competency::record_exists($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Whether the current user can delete an evidence in the context of a user. * * @param int $userid The user ID the evidence belongs to. * @return bool */ public static function can_delete_user($userid) { return has_capability('moodle/competency:evidencedelete', context_user::instance($userid)); } /** * Load a list of records in a context for a user competency. * * @param int $usercompetencyid The id of the user competency. * @param context $context Context to filter the evidence list. * @param string $sort The field from the evidence table to sort on. * @param string $order The sort direction * @param int $skip Limitstart. * @param int $limit Number of rows to return. * * @return \core_competency\persistent[] */ public static function get_records_for_usercompetency($usercompetencyid, \context $context, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) { global $DB; $params = array( 'usercompid' => $usercompetencyid, 'path' => $context->path . '/%', 'contextid' => $context->id ); if (!empty($sort)) { $sort = ' ORDER BY e.' . $sort . ' ' . $order . ', e.id ASC'; } else { $sort = ' ORDER BY e.id ASC'; } $sql = 'SELECT e.* FROM {' . static::TABLE . '} e JOIN {context} c ON c.id = e.contextid WHERE (c.path LIKE :path OR c.id = :contextid) AND e.usercompetencyid = :usercompid ' . $sort; $records = $DB->get_records_sql($sql, $params, $skip, $limit); $instances = array(); foreach ($records as $record) { $newrecord = new static(0, $record); array_push($instances, $newrecord); } return $instances; } } competency_rule.php 0000644 00000006036 15151776373 0010476 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Competency rule base. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use coding_exception; /** * Competency rule base abstract class. * * Rules are attached to a competency and then tested against a user competency * to determine whether or not it matches. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class competency_rule { /** @var competency The competency. */ protected $competency; /** * Constructor. * * @param competency $competency The competency. */ public function __construct(competency $competency) { $class = $competency->get('ruletype'); if (!$class || !($this instanceof $class)) { throw new coding_exception('This competency does not use this rule.'); } $this->competency = $competency; } /** * Get the rule config. * * @return mixed */ protected function get_config() { return $this->competency->get('ruleconfig'); } /** * Whether or not the rule is matched. * * @param user_competency $usercompetency The user competency to test against. * @return bool */ abstract public function matches(user_competency $usercompetency); /** * Validate the rule config. * * @param string $value The value to validate. * @return bool */ abstract public function validate_config($value); /** * The name of the rule. * * @return lang_string */ public static function get_name() { throw new coding_exception('Method not implemented.'); } /** * Migrate rule config from one set of competencies to another. * * Exceptions should be thrown when the migration can not be performed. * * @param string $config Original config rule of a competency. * @param array $mappings Array that matches the original competency IDs with the new competencies objects. * @return string New configuration. * @throws Exception */ public static function migrate_config($config, $mappings) { return $config; } } user_competency_plan.php 0000644 00000027366 15151776373 0011530 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for user_competency_plan persistence. * * @package core_competency * @copyright 2015 Serge Gauthier * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use lang_string; use context_user; /** * Class for loading/storing user_competency_plan from the DB. * * @copyright 2015 Serge Gauthier * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_competency_plan extends persistent { /** Table name for user_competency_plan persistency */ const TABLE = 'competency_usercompplan'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'userid' => array( 'type' => PARAM_INT, ), 'competencyid' => array( 'type' => PARAM_INT, ), 'proficiency' => array( 'type' => PARAM_BOOL, 'default' => null, 'null' => NULL_ALLOWED, ), 'grade' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED, ), 'planid' => array( 'type' => PARAM_INT, ), 'sortorder' => array( 'type' => PARAM_INT, 'default' => 0, ), ); } /** * Return the competency Object. * * @return competency Competency Object */ public function get_competency() { return new competency($this->get('competencyid')); } /** * Get the context. * * @return context The context. */ public function get_context() { return context_user::instance($this->get('userid')); } /** * Validate the user ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_userid($value) { global $DB; if (!$DB->record_exists('user', array('id' => $value))) { return new lang_string('invaliduserid', 'error'); } return true; } /** * Validate the competency ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_competencyid($value) { if (!competency::record_exists($value)) { return new lang_string('errornocompetency', 'core_competency', $value); } return true; } /** * Validate the grade. * * @param int $value The value. * @return true|lang_string */ protected function validate_grade($value) { if ($value !== null) { if ($value <= 0) { return new lang_string('invalidgrade', 'core_competency'); } // TODO MDL-52243 Use a core method to validate the grade_scale item. // Check if grade exist in the scale item values. $competency = $this->get_competency(); if (!array_key_exists($value - 1, $competency->get_scale()->scale_items)) { return new lang_string('invalidgrade', 'core_competency'); } } return true; } /** * Validate the plan ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_planid($value) { if (!plan::record_exists($value) ) { return new lang_string('invalidplan', 'core_competency'); } return true; } /** * Create a new user_competency_plan object. * * Note, this is intended to be used to create a blank relation, for instance when * the record was not found in the database. This does not save the model. * * @param int $userid The user ID. * @param int $competencyid The competency ID. * @param int $planid The plan ID. * @return \core_competency\user_competency_plan */ public static function create_relation($userid, $competencyid, $planid) { $relation = new user_competency_plan(0, (object) array('userid' => $userid, 'competencyid' => $competencyid, 'planid' => $planid)); return $relation; } /** * List the competencies in this plan. * * @param int $planid The plan ID * @param int $userid The user ID * @return competency[] */ public static function list_competencies($planid, $userid) { global $DB; $sql = 'SELECT c.* FROM {' . competency::TABLE . '} c JOIN {' . self::TABLE . '} ucp ON ucp.competencyid = c.id AND ucp.userid = :userid WHERE ucp.planid = :planid ORDER BY ucp.sortorder ASC, ucp.id ASC'; $params = array('userid' => $userid, 'planid' => $planid); $results = $DB->get_recordset_sql($sql, $params); $instances = array(); foreach ($results as $key => $result) { $instances[$key] = new competency(0, $result); } $results->close(); return $instances; } /** * Fetch a competency by plan ID. * * @param int $id The plan ID. * @return competency */ public static function get_competency_by_planid($planid, $competencyid) { global $DB; $sql = "SELECT c.* FROM {" . self::TABLE . "} ucp JOIN {" . competency::TABLE . "} c ON c.id = ucp.competencyid WHERE ucp.planid = ? AND ucp.competencyid = ?"; $record = $DB->get_record_sql($sql, array($planid, $competencyid)); if (!$record) { throw new \coding_exception('The competency does not belong to this plan: ' . $competencyid . ', ' . $planid); } return new competency(0, $record); } /** * Get multiple user_competency_plan for a user. * * @param int $userid The user ID. * @param int $planid The plan ID. * @param array $competenciesorids Limit search to those competencies, or competency IDs. * @return \core_competency\user_competency_plan[] */ public static function get_multiple($userid, $planid, array $competenciesorids = null) { global $DB; $params = array(); $params['userid'] = $userid; $params['planid'] = $planid; $sql = '1 = 1'; if (!empty($competenciesorids)) { $test = reset($competenciesorids); if (is_number($test)) { $ids = $competenciesorids; } else { $ids = array(); foreach ($competenciesorids as $comp) { $ids[] = $comp->get('id'); } } list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); $params += $inparams; $sql = "competencyid $insql"; } // Order by ID to prevent random ordering. return static::get_records_select("userid = :userid AND planid = :planid AND $sql", $params, 'id ASC'); } /** * Checks if a competency has user competency plan records. * * @param int $competencyid The competency ID * @return boolean */ public static function has_records_for_competency($competencyid) { return self::record_exists_select('competencyid = ?', array($competencyid)); } /** * Checks if any of the competencies of a framework has a user competency plan record. * * @param int $frameworkid The competency framework ID. * @return boolean */ public static function has_records_for_framework($frameworkid) { global $DB; $sql = "SELECT 'x' FROM {" . self::TABLE . "} ucp JOIN {" . competency::TABLE . "} c ON ucp.competencyid = c.id WHERE c.competencyframeworkid = ?"; $params = array($frameworkid); return $DB->record_exists_sql($sql, $params); } /** * Check if user competency plan has records for competencies. * * @param array $competencyids The competences IDs * @return boolean */ public static function has_records_for_competencies($competencyids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); return self::record_exists_select("competencyid $insql", $params); } /** * Count the number of records matching a specific template, optionally filtered by proficient values. * * @param int $templateid * @param mixed $proficiency - If true - filter by proficiency, if false filter by not proficient, if null - do not filter. * @return int */ public static function count_records_for_template($templateid, $proficiency=null) { global $DB; $params = array('templateid' => $templateid); $sql = 'SELECT ' . " COUNT('x') " . 'FROM {' . self::TABLE . '} ucp JOIN {' . plan::TABLE . '} p ON ucp.planid = p.id WHERE p.templateid = :templateid'; if ($proficiency === true) { $sql .= ' AND ucp.proficiency = :proficiency'; $params['proficiency'] = true; } else if ($proficiency === false) { $sql .= ' AND (ucp.proficiency = :proficiency OR ucp.proficiency IS NULL)'; $params['proficiency'] = false; } return $DB->count_records_sql($sql, $params); } /** * Get the list of competencies that were completed the least times (in completed plans) from a template. * * @param int $templateid * @param int $skip The number of competencies to skip * @param int $limit The max number of competencies to return * @return competency[] */ public static function get_least_proficient_competencies_for_template($templateid, $skip = 0, $limit = 0) { global $DB; $fields = competency::get_sql_fields('c', 'c_'); $params = array('templateid' => $templateid, 'notproficient' => false); $sql = 'SELECT ' . $fields . ' FROM (SELECT ucp.competencyid, COUNT(ucp.competencyid) AS timesnotproficient FROM {' . self::TABLE . '} ucp JOIN {' . plan::TABLE . '} p ON p.id = ucp.planid WHERE p.templateid = :templateid AND (ucp.proficiency = :notproficient OR ucp.proficiency IS NULL) GROUP BY ucp.competencyid ) p JOIN {' . competency::TABLE . '} c ON c.id = p.competencyid ORDER BY p.timesnotproficient DESC, c.id ASC'; $results = $DB->get_records_sql($sql, $params, $skip, $limit); $comps = array(); foreach ($results as $r) { $c = competency::extract_record($r, 'c_'); $comps[] = new competency(0, $c); } return $comps; } } user_competency.php 0000644 00000040752 15151776373 0010510 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for user_competency persistence. * * @package core_competency * @copyright 2015 Serge Gauthier * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use coding_exception; use context_course; use context_user; use comment; use lang_string; /** * Class for loading/storing user_competency from the DB. * * @copyright 2015 Serge Gauthier * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_competency extends persistent { /** Table name for user_competency persistency */ const TABLE = 'competency_usercomp'; /** Idle status */ const STATUS_IDLE = 0; /** Waiting for review status */ const STATUS_WAITING_FOR_REVIEW = 1; /** In review status */ const STATUS_IN_REVIEW = 2; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'userid' => array( 'type' => PARAM_INT, ), 'competencyid' => array( 'type' => PARAM_INT, ), 'status' => array( 'choices' => array( self::STATUS_IDLE, self::STATUS_WAITING_FOR_REVIEW, self::STATUS_IN_REVIEW, ), 'type' => PARAM_INT, 'default' => self::STATUS_IDLE, ), 'reviewerid' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED, ), 'proficiency' => array( 'type' => PARAM_BOOL, 'default' => null, 'null' => NULL_ALLOWED, ), 'grade' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED, ), ); } /** * Whether the current user can comment on this user competency. * * @return bool */ public function can_comment() { return static::can_comment_user($this->get('userid')); } /** * Whether the current user can read this user competency. * * @return bool */ public function can_read() { return static::can_read_user($this->get('userid')); } /** * Whether the current user can read comments on this user competency. * * @return bool */ public function can_read_comments() { return static::can_read_comments_user($this->get('userid')); } /** * Can the current user send the user competency for review? * * @return bool */ public function can_request_review() { return static::can_request_review_user($this->get('userid')); } /** * Can the current user review the user competency? * * @return bool */ public function can_review() { return static::can_review_user($this->get('userid')); } /** * Human readable status name. * * @param int $status The status code. * @return lang_string */ public static function get_status_name($status) { switch ($status) { case self::STATUS_IDLE: $strname = 'idle'; break; case self::STATUS_WAITING_FOR_REVIEW: $strname = 'waitingforreview'; break; case self::STATUS_IN_REVIEW: $strname = 'inreview'; break; default: throw new \moodle_exception('errorusercomptencystatus', 'core_competency', '', $status); break; } return new lang_string('usercompetencystatus_' . $strname, 'core_competency'); } /** * Get list of competency status. * * @return array */ public static function get_status_list() { static $list = null; if ($list === null) { $list = array( self::STATUS_IDLE => self::get_status_name(self::STATUS_IDLE), self::STATUS_WAITING_FOR_REVIEW => self::get_status_name(self::STATUS_WAITING_FOR_REVIEW), self::STATUS_IN_REVIEW => self::get_status_name(self::STATUS_IN_REVIEW)); } return $list; } /** * Get the comment object. * * @return comment */ public function get_comment_object() { global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); if (!$this->get('id')) { throw new coding_exception('The user competency record must exist.'); } $comment = new comment((object) array( 'context' => $this->get_context(), 'component' => 'competency', // This cannot be named 'core_competency'. 'itemid' => $this->get('id'), 'area' => 'user_competency', 'showcount' => true, )); $comment->set_fullwidth(true); return $comment; } /** * Return the competency Object. * * @return competency Competency Object */ public function get_competency() { return new competency($this->get('competencyid')); } /** * Get the context. * * @return context The context. */ public function get_context() { return context_user::instance($this->get('userid')); } /** * Find the plans for the user and this competency. * * Note that this: * - does not perform any capability check. * - may return completed plans. * - may return an empty array. * * @return plans[] */ public function get_plans() { return plan::get_by_user_and_competency($this->get('userid'), $this->get('competencyid')); } /** * Validate the user ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_userid($value) { global $DB; if (!$DB->record_exists('user', array('id' => $value))) { return new lang_string('invaliduserid', 'error'); } return true; } /** * Validate the competency ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_competencyid($value) { if (!competency::record_exists($value)) { return new lang_string('errornocompetency', 'core_competency', $value); } return true; } /** * Validate the proficiency. * * @param int $value The value. * @return true|lang_string */ protected function validate_proficiency($value) { $grade = $this->get('grade'); if ($grade !== null && $value === null) { // We must set a proficiency when we set a grade. return new lang_string('invaliddata', 'error'); } else if ($grade === null && $value !== null) { // We must not set a proficiency when we don't set a grade. return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the reviewer ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_reviewerid($value) { global $DB; if ($value !== null && !$DB->record_exists('user', array('id' => $value))) { return new lang_string('invaliduserid', 'error'); } return true; } /** * Validate the grade. * * @param int $value The value. * @return true|lang_string */ protected function validate_grade($value) { if ($value !== null) { if ($value <= 0) { return new lang_string('invalidgrade', 'core_competency'); } // TODO MDL-52243 Use a core method to validate the grade_scale item. // Check if grade exist in the scale item values. $competency = $this->get_competency(); if (!array_key_exists($value - 1 , $competency->get_scale()->scale_items)) { return new lang_string('invalidgrade', 'core_competency'); } } return true; } /** * Can the current user comment on a user's competency? * * @param int $userid The user ID the competency belongs to. * @return bool */ public static function can_comment_user($userid) { global $USER; $capabilities = array('moodle/competency:usercompetencycomment'); if ($USER->id == $userid) { $capabilities[] = 'moodle/competency:usercompetencycommentown'; } if (has_any_capability($capabilities, context_user::instance($userid))) { return true; } return false; } /** * Can the current user grade a user's user competency? * * @param int $userid The user ID the competency belongs to. * @return bool */ public static function can_grade_user($userid) { $ratecap = 'moodle/competency:competencygrade'; return has_capability($ratecap, context_user::instance($userid)); } /** * Can the current user grade a user's user competency in a course? * * @param int $userid The user ID the competency belongs to. * @param int $courseid The course ID. * @return bool */ public static function can_grade_user_in_course($userid, $courseid) { $ratecap = 'moodle/competency:competencygrade'; return has_capability($ratecap, context_course::instance($courseid)) || static::can_grade_user($userid); } /** * Can the current user read the comments on a user's competency? * * @param int $userid The user ID the competency belongs to. * @return bool */ public static function can_read_comments_user($userid) { // Everyone who can read the user competency can read the comments. return static::can_read_user($userid); } /** * Can the current user read the user competencies of a user in a course? * * @param int $userid The user ID the competency belongs to. * @param int $courseid The course ID. * @return bool */ public static function can_read_user_in_course($userid, $courseid) { $capability = 'moodle/competency:usercompetencyview'; return has_capability($capability, context_course::instance($courseid)) || static::can_read_user($userid); } /** * Can the current user read a user's competency? * * @param int $userid The user ID the competency belongs to. * @return bool */ public static function can_read_user($userid) { $capability = 'moodle/competency:usercompetencyview'; return has_capability($capability, context_user::instance($userid)) || plan::can_read_user($userid); } /** * Can the current user send a user's competency for review? * * Note that the status 'review' is not meant to be used for student to self-assess * themselves, then to ask the teacher to review their assessment. It is more intended * for a student to provide evidence of prior learning and request their review. * * @param int $userid The user ID the competency belongs to. * @return bool */ public static function can_request_review_user($userid) { global $USER; $capabilities = array('moodle/competency:usercompetencyrequestreview'); if ($USER->id == $userid) { $capabilities[] = 'moodle/competency:usercompetencyrequestreviewown'; } if (has_any_capability($capabilities, context_user::instance($userid))) { return true; } return false; } /** * Can the current user review the user competency? * * @param int $userid The user ID the competency belongs to. * @return bool */ public static function can_review_user($userid) { $capability = 'moodle/competency:usercompetencyreview'; return has_capability($capability, context_user::instance($userid)); } /** * Create a new user_competency object. * * Note, this is intended to be used to create a blank relation, for instance when * the record was not found in the database. This does not save the model. * * @param int $userid The user ID. * @param int $competencyid The competency ID. * @return \core_competency\user_competency */ public static function create_relation($userid, $competencyid) { $relation = new user_competency(0, (object) array('userid' => $userid, 'competencyid' => $competencyid)); return $relation; } /** * Fetch a competency by user competency ID. * * This is a convenience method to attempt to efficiently fetch a competency when * the only information we have is the user_competency ID, in evidence for instance. * * @param int $id The user competency ID. * @return competency */ public static function get_competency_by_usercompetencyid($id) { global $DB; $sql = "SELECT c.* FROM {" . self::TABLE . "} uc JOIN {" . competency::TABLE . "} c ON c.id = uc.competencyid WHERE uc.id = ?"; $record = $DB->get_record_sql($sql, array($id), MUST_EXIST); return new competency(0, $record); } /** * Get multiple user_competency for a user. * * @param int $userid * @param array $competenciesorids Limit search to those competencies, or competency IDs. * @return \core_competency\user_competency[] */ public static function get_multiple($userid, array $competenciesorids = null) { global $DB; $params = array(); $params['userid'] = $userid; $sql = '1 = 1'; if (!empty($competenciesorids)) { $test = reset($competenciesorids); if (is_number($test)) { $ids = $competenciesorids; } else { $ids = array(); foreach ($competenciesorids as $comp) { $ids[] = $comp->get('id'); } } list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); $params += $inparams; $sql = "competencyid $insql"; } // Order by ID to prevent random ordering. return self::get_records_select("userid = :userid AND $sql", $params, 'id ASC'); } /** * Checks if a competency has user competency records. * * @param int $competencyid The competency ID * @return boolean */ public static function has_records_for_competency($competencyid) { return self::record_exists_select('competencyid = ?', array($competencyid)); } /** * Checks if any of the competencies of a framework has a user competency record. * * @param int $frameworkid The competency framework ID. * @return boolean */ public static function has_records_for_framework($frameworkid) { global $DB; $sql = "SELECT 'x' FROM {" . self::TABLE . "} uc JOIN {" . competency::TABLE . "} c ON uc.competencyid = c.id WHERE c.competencyframeworkid = ?"; $params = array($frameworkid); return $DB->record_exists_sql($sql, $params); } /** * Check if user competency has records for competencies. * * @param array $competencyids The competencies ids. * @return boolean Return true if the delete was successfull. */ public static function has_records_for_competencies($competencyids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); return self::record_exists_select("competencyid $insql", $params); } } template_cohort.php 0000644 00000020056 15151776373 0010470 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Template cohort persistent. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use lang_string; use core_competency\template; /** * Template cohort persistent. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_cohort extends persistent { const TABLE = 'competency_templatecohort'; /** 2 hours of threshold to prevent expired plans **/ const DUEDATE_THRESHOLD = 7200; /** * Return the custom definition of the properties of this model. * * @return array Where keys are the property names. */ protected static function define_properties() { return array( 'templateid' => array( 'type' => PARAM_INT ), 'cohortid' => array( 'type' => PARAM_INT ) ); } /** * Validate the cohort ID. * * @param int $value The cohort ID. * @return true|lang_string */ protected function validate_cohortid($value) { global $DB; if (!$DB->record_exists('cohort', array('id' => $value))) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the template ID. * * @param int $value The template ID. * @return true|lang_string */ protected function validate_templateid($value) { if (!template::record_exists($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Return an array of user IDs for which the plans are missing. * * Plans are considered as missing when a member of a cohort does not have a plan created. * When the parameter $unlinkedaremissing is set to false, plans that were unlinked from * their template will be ignored so that we do not recreate unlinked plans endlessly. * * This method ignores the due date of the template. * * @param int $templateid The template ID. * @param int $cohortid The cohort ID. * @param boolean $unlinkedaremissing When true, unlinked plans are considered as missing. * @return int[] User IDs. */ public static function get_missing_plans($templateid, $cohortid, $unlinkedaremissing = false) { global $DB; $skipsql = ''; $skipparams = array(); if (!$unlinkedaremissing) { $skipsql = 'OR p.origtemplateid = :origtemplateid'; $skipparams = array('origtemplateid' => $templateid); } $sql = "SELECT cm.userid FROM {cohort_members} cm LEFT JOIN {" . plan::TABLE . "} p ON p.userid = cm.userid AND (p.templateid = :templateid $skipsql) WHERE cm.cohortid = :cohortid AND p.id IS NULL"; $params = array('templateid' => $templateid, 'cohortid' => $cohortid) + $skipparams; return $DB->get_fieldset_sql($sql, $params); } /** * Get a relation. * * This does not perform any validation on the data passed. If the relation exists in the database * then it is loaded in a the model, if not then it is up to the developer to save the model. * * @param int $templateid * @param int $cohortid * @return template_cohort */ public static function get_relation($templateid, $cohortid) { global $DB; $params = array( 'templateid' => $templateid, 'cohortid' => $cohortid ); $relation = new static(null, (object) $params); if ($record = $DB->get_record(self::TABLE, $params)) { $relation->from_record($record); } return $relation; } /** * Get a relations by templateid. * * This does not perform any validation on the data passed. If the relation exists in the database * then it is loaded in a the model, if not then it is up to the developer to save the model. * * @param int $templateid * @return template_cohort[] array of template cohort */ public static function get_relations_by_templateid($templateid) { global $DB; $params = array( 'templateid' => $templateid ); $relations = array(); $records = $DB->get_records(self::TABLE, $params); foreach ($records as $record) { $relations[] = new template_cohort(0, $record); } return $relations; } /** * Return an array of templates persistent with their missing userids. * * Note that only cohorts associated with visible templates are considered, * as well as only templates with a due date in the future, or no due date. * * @param int $lastruntime The last time the Cohort ssync task ran. * @param bool $unlinkedaremissing When true, unlinked plans are considered as missing. * @return array( array( * 'template' => \core_competency\template, * 'userids' => array * )) */ public static function get_all_missing_plans($lastruntime = 0, $unlinkedaremissing = false) { global $DB; $planwhereclause = " WHERE (p.id is NULL AND (cm.timeadded >= :lastruntime1 OR tc.timecreated >= :lastruntime3 OR t.timemodified >= :lastruntime4))"; if ($unlinkedaremissing) { $planwhereclause .= " OR (p.origtemplateid IS NOT NULL AND cm.timeadded < :lastruntime2)"; } $sql = "SELECT " . $DB->sql_concat('cm.userid', 'tc.templateid') . " as uniqueid, cm.userid, t.* FROM {cohort_members} cm JOIN {" . self::TABLE . "} tc ON cm.cohortid = tc.cohortid JOIN {" . template::TABLE . "} t ON (tc.templateid = t.id AND t.visible = 1) AND (t.duedate = 0 OR t.duedate > :time1) LEFT JOIN {" . plan::TABLE . "} p ON (cm.userid = p.userid AND (t.id = p.templateid OR t.id = p.origtemplateid)) $planwhereclause ORDER BY t.id"; $params = array('time1' => time(), 'time2' => time(), 'lastruntime1' => $lastruntime, 'lastruntime2' => $lastruntime, 'lastruntime3' => $lastruntime, 'lastruntime4' => $lastruntime); $results = $DB->get_records_sql($sql, $params); $missingplans = array(); foreach ($results as $usertemplate) { $userid = $usertemplate->userid; // Check if template already exist in the array. if (isset($missingplans[$usertemplate->id])) { $missingplans[$usertemplate->id]['userids'][] = $userid; } else { unset($usertemplate->userid); unset($usertemplate->uniqueid); $template = new template(0, $usertemplate); $missingplans[$template->get('id')]['template'] = $template; $missingplans[$template->get('id')]['userids'][] = $userid; } } return array_values($missingplans); } } competency.php 0000644 00000067326 15151776373 0007460 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing competencies from the DB. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use coding_exception; use context_system; use lang_string; use stdClass; require_once($CFG->libdir . '/grade/grade_scale.php'); /** * Class for loading/storing competencies from the DB. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class competency extends persistent { const TABLE = 'competency'; /** Outcome none. */ const OUTCOME_NONE = 0; /** Outcome evidence. */ const OUTCOME_EVIDENCE = 1; /** Outcome complete. */ const OUTCOME_COMPLETE = 2; /** Outcome recommend. */ const OUTCOME_RECOMMEND = 3; /** @var competency Object before update. */ protected $beforeupdate = null; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'shortname' => array( 'type' => PARAM_TEXT ), 'idnumber' => array( 'type' => PARAM_RAW ), 'description' => array( 'default' => '', 'type' => PARAM_CLEANHTML ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML ), 'sortorder' => array( 'default' => 0, 'type' => PARAM_INT ), 'parentid' => array( 'default' => 0, 'type' => PARAM_INT ), 'path' => array( 'default' => '/0/', 'type' => PARAM_RAW ), 'ruleoutcome' => array( 'choices' => array(self::OUTCOME_NONE, self::OUTCOME_EVIDENCE, self::OUTCOME_COMPLETE, self::OUTCOME_RECOMMEND), 'default' => self::OUTCOME_NONE, 'type' => PARAM_INT ), 'ruletype' => array( 'type' => PARAM_RAW, 'default' => null, 'null' => NULL_ALLOWED ), 'ruleconfig' => array( 'default' => null, 'type' => PARAM_RAW, 'null' => NULL_ALLOWED ), 'scaleid' => array( 'default' => null, 'type' => PARAM_INT, 'null' => NULL_ALLOWED ), 'scaleconfiguration' => array( 'default' => null, 'type' => PARAM_RAW, 'null' => NULL_ALLOWED ), 'competencyframeworkid' => array( 'default' => 0, 'type' => PARAM_INT ), ); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { $this->beforeupdate = null; $this->newparent = null; // During update. if ($this->get('id')) { $this->beforeupdate = new competency($this->get('id')); // The parent ID has changed. if ($this->beforeupdate->get('parentid') != $this->get('parentid')) { $this->newparent = $this->get_parent(); // Update path and sortorder. $this->set_new_path($this->newparent); $this->set_new_sortorder(); } } else { // During create. $this->set_new_path(); // Always generate new sortorder when we create new competency. $this->set_new_sortorder(); } } /** * Hook to execute after an update. * * @param bool $result Whether or not the update was successful. * @return void */ protected function after_update($result) { global $DB; if (!$result) { $this->beforeupdate = null; return; } // The parent ID has changed, we need to fix all the paths of the children. if ($this->beforeupdate->get('parentid') != $this->get('parentid')) { $beforepath = $this->beforeupdate->get('path') . $this->get('id') . '/'; $like = $DB->sql_like('path', '?'); $likesearch = $DB->sql_like_escape($beforepath) . '%'; $table = '{' . self::TABLE . '}'; $sql = "UPDATE $table SET path = REPLACE(path, ?, ?) WHERE " . $like; $DB->execute($sql, array( $beforepath, $this->get('path') . $this->get('id') . '/', $likesearch )); // Resolving sortorder holes left after changing parent. $table = '{' . self::TABLE . '}'; $sql = "UPDATE $table SET sortorder = sortorder -1 " . " WHERE competencyframeworkid = ? AND parentid = ? AND sortorder > ?"; $DB->execute($sql, array($this->get('competencyframeworkid'), $this->beforeupdate->get('parentid'), $this->beforeupdate->get('sortorder') )); } $this->beforeupdate = null; } /** * Hook to execute after a delete. * * @param bool $result Whether or not the delete was successful. * @return void */ protected function after_delete($result) { global $DB; if (!$result) { return; } // Resolving sortorder holes left after delete. $table = '{' . self::TABLE . '}'; $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE competencyframeworkid = ? AND parentid = ? AND sortorder > ?"; $DB->execute($sql, array($this->get('competencyframeworkid'), $this->get('parentid'), $this->get('sortorder'))); } /** * Extracts the default grade from the scale configuration. * * Returns an array where the first element is the grade, and the second * is a boolean representing whether or not this grade is considered 'proficient'. * * @return array(int grade, bool proficient) */ public function get_default_grade() { $scaleid = $this->get('scaleid'); $scaleconfig = $this->get('scaleconfiguration'); if ($scaleid === null) { $scaleconfig = $this->get_framework()->get('scaleconfiguration'); } return competency_framework::get_default_grade_from_scale_configuration($scaleconfig); } /** * Get the competency framework. * * @return competency_framework */ public function get_framework() { return new competency_framework($this->get('competencyframeworkid')); } /** * Get the competency level. * * @return int */ public function get_level() { $path = $this->get('path'); $path = trim($path, '/'); return substr_count($path, '/') + 1; } /** * Return the parent competency. * * @return null|competency */ public function get_parent() { $parentid = $this->get('parentid'); if (!$parentid) { return null; } return new competency($parentid); } /** * Extracts the proficiency of a grade from the scale configuration. * * @param int $grade The grade (scale item ID). * @return array(int grade, bool proficient) */ public function get_proficiency_of_grade($grade) { $scaleid = $this->get('scaleid'); $scaleconfig = $this->get('scaleconfiguration'); if ($scaleid === null) { $scaleconfig = $this->get_framework()->get('scaleconfiguration'); } return competency_framework::get_proficiency_of_grade_from_scale_configuration($scaleconfig, $grade); } /** * Return the related competencies. * * @return competency[] */ public function get_related_competencies() { return related_competency::get_related_competencies($this->get('id')); } /** * Get the rule object. * * @return null|competency_rule */ public function get_rule_object() { $rule = $this->get('ruletype'); if (!$rule || !is_subclass_of($rule, 'core_competency\\competency_rule')) { // Double check that the rule is extending the right class to avoid bad surprises. return null; } return new $rule($this); } /** * Return the scale. * * @return \grade_scale */ public function get_scale() { $scaleid = $this->get('scaleid'); if ($scaleid === null) { return $this->get_framework()->get_scale(); } $scale = \grade_scale::fetch(array('id' => $scaleid)); $scale->load_items(); return $scale; } /** * Returns true when the competency has user competencies. * * This is useful to determine if the competency, or part of it, should be locked down. * * @return boolean */ public function has_user_competencies() { return user_competency::has_records_for_competency($this->get('id')) || user_competency_plan::has_records_for_competency($this->get('id')); } /** * Check if the competency is the parent of passed competencies. * * @param array $ids IDs of supposedly direct children. * @return boolean */ public function is_parent_of(array $ids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); $params['parentid'] = $this->get('id'); return $DB->count_records_select(self::TABLE, "id $insql AND parentid = :parentid", $params) == count($ids); } /** * Reset the rule. * * @return void */ public function reset_rule() { $this->raw_set('ruleoutcome', static::OUTCOME_NONE); $this->raw_set('ruletype', null); $this->raw_set('ruleconfig', null); } /** * Helper method to set the path. * * @param competency $parent The parent competency object. * @return void */ protected function set_new_path(competency $parent = null) { $path = '/0/'; if ($this->get('parentid')) { $parent = $parent !== null ? $parent : $this->get_parent(); $path = $parent->get('path') . $this->get('parentid') . '/'; } $this->raw_set('path', $path); } /** * Helper method to set the sortorder. * * @return void */ protected function set_new_sortorder() { $search = array('parentid' => $this->get('parentid'), 'competencyframeworkid' => $this->get('competencyframeworkid')); $this->raw_set('sortorder', $this->count_records($search)); } /** * This does a specialised search that finds all nodes in the tree with matching text on any text like field, * and returns this node and all its parents in a displayable sort order. * * @param string $searchtext The text to search for. * @param int $competencyframeworkid The competency framework to limit the search. * @return persistent[] */ public static function search($searchtext, $competencyframeworkid) { global $DB; $like1 = $DB->sql_like('shortname', ':like1', false); $like2 = $DB->sql_like('idnumber', ':like2', false); $like3 = $DB->sql_like('description', ':like3', false); $params = array( 'like1' => '%' . $DB->sql_like_escape($searchtext) . '%', 'like2' => '%' . $DB->sql_like_escape($searchtext) . '%', 'like3' => '%' . $DB->sql_like_escape($searchtext) . '%', 'frameworkid' => $competencyframeworkid ); $sql = 'competencyframeworkid = :frameworkid AND ((' . $like1 . ') OR (' . $like2 . ') OR (' . $like3 . '))'; $records = $DB->get_records_select(self::TABLE, $sql, $params, 'path, sortorder ASC', '*'); // Now get all the parents. $parents = array(); foreach ($records as $record) { $split = explode('/', trim($record->path, '/')); foreach ($split as $parent) { $parents[intval($parent)] = true; } } $parents = array_keys($parents); // Skip ones we already fetched. foreach ($parents as $idx => $parent) { if ($parent == 0 || isset($records[$parent])) { unset($parents[$idx]); } } if (count($parents)) { list($parentsql, $parentparams) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED); $parentrecords = $DB->get_records_select(self::TABLE, 'id ' . $parentsql, $parentparams, 'path, sortorder ASC', '*'); foreach ($parentrecords as $id => $record) { $records[$id] = $record; } } $instances = array(); // Convert to instances of this class. foreach ($records as $record) { $newrecord = new static(0, $record); $instances[$newrecord->get('id')] = $newrecord; } return $instances; } /** * Validate the competency framework ID. * * @param int $value The framework ID. * @return true|lang_string */ protected function validate_competencyframeworkid($value) { // During update. if ($this->get('id')) { // Ensure that we are not trying to move the competency across frameworks. if ($this->beforeupdate->get('competencyframeworkid') != $value) { return new lang_string('invaliddata', 'error'); } } else { // During create. // Check that the framework exists. if (!competency_framework::record_exists($value)) { return new lang_string('invaliddata', 'error'); } } return true; } /** * Validate the ID number. * * @param string $value The ID number. * @return true|lang_string */ protected function validate_idnumber($value) { global $DB; $sql = 'idnumber = :idnumber AND competencyframeworkid = :competencyframeworkid AND id <> :id'; $params = array( 'id' => $this->get('id'), 'idnumber' => $value, 'competencyframeworkid' => $this->get('competencyframeworkid') ); if ($DB->record_exists_select(self::TABLE, $sql, $params)) { return new lang_string('idnumbertaken', 'error'); } return true; } /** * Validate the path. * * @param string $value The path. * @return true|lang_string */ protected function validate_path($value) { // The last item should be the parent ID. $id = $this->get('parentid'); if (substr($value, -(strlen($id) + 2)) != '/' . $id . '/') { return new lang_string('invaliddata', 'error'); } else if (!preg_match('@/([0-9]+/)+@', $value)) { // The format of the path is not correct. return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the parent ID. * * @param string $value The ID. * @return true|lang_string */ protected function validate_parentid($value) { // Check that the parent exists. But only if we don't have it already, and we actually have a parent. if (!empty($value) && !$this->newparent && !self::record_exists($value)) { return new lang_string('invaliddata', 'error'); } // During update. if ($this->get('id')) { // If there is a new parent. if ($this->beforeupdate->get('parentid') != $value && $this->newparent) { // Check that the new parent belongs to the same framework. if ($this->newparent->get('competencyframeworkid') != $this->get('competencyframeworkid')) { return new lang_string('invaliddata', 'error'); } } } return true; } /** * Validate the rule. * * @param string $value The ID. * @return true|lang_string */ protected function validate_ruletype($value) { if ($value === null) { return true; } if (!class_exists($value) || !is_subclass_of($value, 'core_competency\\competency_rule')) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the rule config. * * @param string $value The ID. * @return true|lang_string */ protected function validate_ruleconfig($value) { $rule = $this->get_rule_object(); // We don't have a rule. if (empty($rule)) { if ($value === null) { // No config, perfect. return true; } // Config but no rules, whoops! return new lang_string('invaliddata', 'error'); } $valid = $rule->validate_config($value); if ($valid !== true) { // Whoops! return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the scale ID. * * Note that the value for a scale can never be 0, null has to be used when * the framework's scale has to be used. * * @param int $value * @return true|lang_string */ protected function validate_scaleid($value) { global $DB; if ($value === null) { return true; } // Always validate that the scale exists. if (!$DB->record_exists_select('scale', 'id = :id', array('id' => $value))) { return new lang_string('invalidscaleid', 'error'); } // During update. if ($this->get('id')) { // Validate that we can only change the scale when it is not used yet. if ($this->beforeupdate->get('scaleid') != $value) { if ($this->has_user_competencies()) { return new lang_string('errorscalealreadyused', 'core_competency'); } } } return true; } /** * Validate the scale configuration. * * This logic is adapted from {@link \core_competency\competency_framework::validate_scaleconfiguration()}. * * @param string $value The scale configuration. * @return bool|lang_string */ protected function validate_scaleconfiguration($value) { $scaleid = $this->get('scaleid'); if ($scaleid === null && $value === null) { return true; } $scaledefaultselected = false; $proficientselected = false; $scaleconfigurations = json_decode($value); if (is_array($scaleconfigurations)) { // The first element of the array contains the scale ID. $scaleinfo = array_shift($scaleconfigurations); if (empty($scaleinfo) || !isset($scaleinfo->scaleid) || $scaleinfo->scaleid != $scaleid) { // This should never happen. return new lang_string('errorscaleconfiguration', 'core_competency'); } // Walk through the array to find proficient and default values. foreach ($scaleconfigurations as $scaleconfiguration) { if (isset($scaleconfiguration->scaledefault) && $scaleconfiguration->scaledefault) { $scaledefaultselected = true; } if (isset($scaleconfiguration->proficient) && $scaleconfiguration->proficient) { $proficientselected = true; } } } if (!$scaledefaultselected || !$proficientselected) { return new lang_string('errorscaleconfiguration', 'core_competency'); } return true; } /** * Return whether or not the competency IDs share the same framework. * * @param array $ids Competency IDs * @return bool */ public static function share_same_framework(array $ids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($ids); $sql = "SELECT COUNT('x') FROM (SELECT DISTINCT(competencyframeworkid) FROM {" . self::TABLE . "} WHERE id {$insql}) f"; return $DB->count_records_sql($sql, $params) == 1; } /** * Get the available rules. * * @return array Keys are the class names, values are the name of the rule. */ public static function get_available_rules() { // Fully qualified class names without leading slashes because get_class() does not add them either. $rules = array( 'core_competency\\competency_rule_all' => competency_rule_all::get_name(), 'core_competency\\competency_rule_points' => competency_rule_points::get_name(), ); return $rules; } /** * Return the current depth of a competency framework. * * @param int $frameworkid The framework ID. * @return int */ public static function get_framework_depth($frameworkid) { global $DB; $totallength = $DB->sql_length('path'); $trimmedlength = $DB->sql_length("REPLACE(path, '/', '')"); $sql = "SELECT ($totallength - $trimmedlength - 1) AS depth FROM {" . self::TABLE . "} WHERE competencyframeworkid = :id ORDER BY depth DESC"; $record = $DB->get_record_sql($sql, array('id' => $frameworkid), IGNORE_MULTIPLE); if (!$record) { $depth = 0; } else { $depth = $record->depth; } return $depth; } /** * Build a framework tree with competency nodes. * * @param int $frameworkid the framework id * @return node[] tree of framework competency nodes */ public static function get_framework_tree($frameworkid) { $competencies = self::search('', $frameworkid); return self::build_tree($competencies, 0); } /** * Get the context from the framework. * * @return context */ public function get_context() { return $this->get_framework()->get_context(); } /** * Recursively build up the tree of nodes. * * @param array $all - List of all competency classes. * @param int $parentid - The current parent ID. Pass 0 to build the tree from the top. * @return node[] $tree tree of nodes */ protected static function build_tree($all, $parentid) { $tree = array(); foreach ($all as $one) { if ($one->get('parentid') == $parentid) { $node = new stdClass(); $node->competency = $one; $node->children = self::build_tree($all, $one->get('id')); $tree[] = $node; } } return $tree; } /** * Check if we can delete competencies safely. * * This moethod does not check any capablities. * Check if competency is used in a plan and user competency. * Check if competency is used in a template. * Check if competency is linked to a course. * * @param array $ids Array of competencies ids. * @return bool True if we can delete the competencies. */ public static function can_all_be_deleted($ids) { global $CFG; if (empty($ids)) { return true; } // Check if competency is used in template. if (template_competency::has_records_for_competencies($ids)) { return false; } // Check if competency is used in plan. if (plan_competency::has_records_for_competencies($ids)) { return false; } // Check if competency is used in course. if (course_competency::has_records_for_competencies($ids)) { return false; } // Check if competency is used in user_competency. if (user_competency::has_records_for_competencies($ids)) { return false; } // Check if competency is used in user_competency_plan. if (user_competency_plan::has_records_for_competencies($ids)) { return false; } require_once($CFG->libdir . '/badgeslib.php'); // Check if competency is used in a badge. if (badge_award_criteria_competency_has_records_for_competencies($ids)) { return false; } return true; } /** * Delete the competencies. * * This method is reserved to core usage. * This method does not trigger the after_delete event. * This method does not delete related objects such as related competencies and evidences. * * @param array $ids The competencies ids. * @return bool True if the competencies were deleted successfully. */ public static function delete_multiple($ids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); return $DB->delete_records_select(self::TABLE, "id $insql", $params); } /** * Get descendant ids. * * @param competency $competency The competency. * @return array Array of competencies ids. */ public static function get_descendants_ids($competency) { global $DB; $path = $DB->sql_like_escape($competency->get('path') . $competency->get('id') . '/') . '%'; $like = $DB->sql_like('path', ':likepath'); return $DB->get_fieldset_select(self::TABLE, 'id', $like, array('likepath' => $path)); } /** * Get competencyids by frameworkid. * * @param int $frameworkid The competency framework ID. * @return array Array of competency ids. */ public static function get_ids_by_frameworkid($frameworkid) { global $DB; return $DB->get_fieldset_select(self::TABLE, 'id', 'competencyframeworkid = :frmid', array('frmid' => $frameworkid)); } /** * Delete competencies by framework ID. * * This method is reserved to core usage. * This method does not trigger the after_delete event. * This method does not delete related objects such as related competencies and evidences. * * @param int $id the framework ID * @return bool Return true if delete was successful. */ public static function delete_by_frameworkid($id) { global $DB; return $DB->delete_records(self::TABLE, array('competencyframeworkid' => $id)); } /** * Get competency ancestors. * * @return competency[] Return array of ancestors. */ public function get_ancestors() { global $DB; $ancestors = array(); $ancestorsids = explode('/', trim($this->get('path'), '/')); // Drop the root item from the array /0/. array_shift($ancestorsids); if (!empty($ancestorsids)) { list($insql, $params) = $DB->get_in_or_equal($ancestorsids, SQL_PARAMS_NAMED); $ancestors = self::get_records_select("id $insql", $params); } return $ancestors; } } template.php 0000644 00000013340 15151776373 0007110 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing learning plan templates from the DB. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use context; use lang_string; use stdClass; /** * Class for loading/storing learning plan templates from the DB. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template extends persistent { const TABLE = 'competency_template'; /** @var template object before update. */ protected $beforeupdate = null; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'shortname' => array( 'type' => PARAM_TEXT, ), 'description' => array( 'default' => '', 'type' => PARAM_CLEANHTML, ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML ), 'duedate' => array( 'default' => 0, 'type' => PARAM_INT, ), 'visible' => array( 'default' => 1, 'type' => PARAM_BOOL, ), 'contextid' => array( 'type' => PARAM_INT ), ); } /** * Hook to execute after an update. * * @param bool $result Whether or not the update was successful. * @return void */ protected function after_update($result) { $this->beforeupdate = null; } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { $this->beforeupdate = null; // During update. if ($this->get('id')) { $this->beforeupdate = new self($this->get('id')); } } /** * Whether or not the current user can read the template. * * @return bool */ public function can_manage() { return self::can_manage_context($this->get_context()); } /** * Whether or not the current user can manage the template. * * @param context $context * @return bool */ public static function can_manage_context($context) { return has_capability('moodle/competency:templatemanage', $context); } /** * Whether or not the current user can read the template. * * @return bool */ public function can_read() { return self::can_read_context($this->get_context()); } /** * Whether or not the current user can read the template. * * @param context $context * @return bool */ public static function can_read_context($context) { return has_capability('moodle/competency:templateview', $context) || self::can_manage_context($context); } /** * Get the context. * * @return context The context */ public function get_context() { return context::instance_by_id($this->get('contextid')); } /** * Validate the context ID. * * @param int $value The context ID. * @return bool|lang_string */ protected function validate_contextid($value) { $context = context::instance_by_id($value, IGNORE_MISSING); if (!$context) { return new lang_string('invalidcontext', 'error'); } else if ($context->contextlevel != CONTEXT_SYSTEM && $context->contextlevel != CONTEXT_COURSECAT) { return new lang_string('invalidcontext', 'error'); } return true; } /** * Validate the due date. * * The due date can always be changed, but when it is it must be: * - unset * - set in the future. * * @param int $value The due date. * @return bool|lang_string */ protected function validate_duedate($value) { // During update. if ($this->get('id')) { $before = $this->beforeupdate->get('duedate'); // The value has not changed, then it's always OK. if ($before == $value) { return true; } } // During create and update, the date must be set in the future, or not set. if (!empty($value) && $value <= time() - 600) { // We cannot set the date in the past. But we allow for 10 minutes of margin so that // a user can set the due date to "now" without risking to hit a validation error. return new lang_string('errorcannotsetduedateinthepast', 'core_competency'); } return true; } /** * Returns true when the template has user learning plans. * * @return boolean */ public function has_plans() { return plan::has_records_for_template($this->get('id')); } } plan.php 0000644 00000053027 15151776373 0006235 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for plans persistence. * * @package core_competency * @copyright 2015 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use comment; use context_user; use dml_missing_record_exception; use lang_string; /** * Class for loading/storing plans from the DB. * * @copyright 2015 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plan extends persistent { const TABLE = 'competency_plan'; /** Draft status. */ const STATUS_DRAFT = 0; /** Active status. */ const STATUS_ACTIVE = 1; /** Complete status. */ const STATUS_COMPLETE = 2; /** Waiting for review. */ const STATUS_WAITING_FOR_REVIEW = 3; /** In review. */ const STATUS_IN_REVIEW = 4; /** 10 minutes threshold **/ const DUEDATE_THRESHOLD = 600; /** @var plan object before update. */ protected $beforeupdate = null; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'name' => array( 'type' => PARAM_TEXT, ), 'description' => array( 'type' => PARAM_CLEANHTML, 'default' => '' ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML, ), 'userid' => array( 'type' => PARAM_INT, ), 'templateid' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED, ), 'origtemplateid' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED, ), 'status' => array( 'choices' => array(self::STATUS_DRAFT, self::STATUS_COMPLETE, self::STATUS_ACTIVE, self::STATUS_WAITING_FOR_REVIEW, self::STATUS_IN_REVIEW), 'type' => PARAM_INT, 'default' => self::STATUS_DRAFT, ), 'duedate' => array( 'type' => PARAM_INT, 'default' => 0, ), 'reviewerid' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED, ) ); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { $this->beforeupdate = null; // During update. if ($this->get('id')) { $this->beforeupdate = new self($this->get('id')); } } /** * Whether the current user can comment on this plan. * * @return bool */ public function can_comment() { return static::can_comment_user($this->get('userid')); } /** * Whether the current user can manage the plan. * * @return bool */ public function can_manage() { if ($this->is_draft()) { return self::can_manage_user_draft($this->get('userid')); } return self::can_manage_user($this->get('userid')); } /** * Whether the current user can read the plan. * * @return bool */ public function can_read() { if ($this->is_draft()) { return self::can_read_user_draft($this->get('userid')); } return self::can_read_user($this->get('userid')); } /** * Whether the current user can read comments on this plan. * * @return bool */ public function can_read_comments() { return $this->can_read(); } /** * Whether the current user can request a review of the plan. * * @return bool */ public function can_request_review() { return self::can_request_review_user($this->get('userid')); } /** * Whether the current user can review the plan. * * @return bool */ public function can_review() { return self::can_review_user($this->get('userid')); } /** * Get the comment object. * * @return comment */ public function get_comment_object() { global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); if (!$this->get('id')) { throw new \coding_exception('The plan must exist.'); } $comment = new comment((object) array( 'client_id' => 'plancommentarea' . $this->get('id'), 'context' => $this->get_context(), 'component' => 'competency', // This cannot be named 'core_competency'. 'itemid' => $this->get('id'), 'area' => 'plan', 'showcount' => true, )); $comment->set_fullwidth(true); return $comment; } /** * Get the competencies in this plan. * * @return competency[] */ public function get_competencies() { $competencies = array(); if ($this->get('status') == self::STATUS_COMPLETE) { // Get the competencies from the archive of the plan. $competencies = user_competency_plan::list_competencies($this->get('id'), $this->get('userid')); } else if ($this->is_based_on_template()) { // Get the competencies from the template. $competencies = template_competency::list_competencies($this->get('templateid')); } else { // Get the competencies from the plan. $competencies = plan_competency::list_competencies($this->get('id')); } return $competencies; } /** * Get a single competency from this plan. * * This will throw an exception if the competency does not belong to the plan. * * @param int $competencyid The competency ID. * @return competency */ public function get_competency($competencyid) { $competency = null; if ($this->get('status') == self::STATUS_COMPLETE) { // Get the competency from the archive of the plan. $competency = user_competency_plan::get_competency_by_planid($this->get('id'), $competencyid); } else if ($this->is_based_on_template()) { // Get the competency from the template. $competency = template_competency::get_competency($this->get('templateid'), $competencyid); } else { // Get the competency from the plan. $competency = plan_competency::get_competency($this->get('id'), $competencyid); } return $competency; } /** * Get the context in which the plan is attached. * * @return context_user */ public function get_context() { return context_user::instance($this->get('userid')); } /** * Human readable status name. * * @return string */ public function get_statusname() { $status = $this->get('status'); switch ($status) { case self::STATUS_DRAFT: $strname = 'draft'; break; case self::STATUS_IN_REVIEW: $strname = 'inreview'; break; case self::STATUS_WAITING_FOR_REVIEW: $strname = 'waitingforreview'; break; case self::STATUS_ACTIVE: $strname = 'active'; break; case self::STATUS_COMPLETE: $strname = 'complete'; break; default: throw new \moodle_exception('errorplanstatus', 'core_competency', '', $status); break; } return get_string('planstatus' . $strname, 'core_competency'); } /** * Get the plan template. * * @return template|null */ public function get_template() { $templateid = $this->get('templateid'); if ($templateid === null) { return null; } return new template($templateid); } /** * Is the plan in draft mode? * * This method is convenient to know if the plan is a draft because whilst a draft * is being reviewed its status is not "draft" any more, but it still is a draft nonetheless. * * @return boolean */ public function is_draft() { return in_array($this->get('status'), static::get_draft_statuses()); } /** * Validate the template ID. * * @param mixed $value The value. * @return true|lang_string */ protected function validate_templateid($value) { // Checks that the template exists. if (!empty($value) && !template::record_exists($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the user ID. * * @param int $value * @return true|lang_string */ protected function validate_userid($value) { global $DB; // During create. if (!$this->get('id')) { // Check that the user exists. We do not need to do that on update because // the userid of a plan should never change. if (!$DB->record_exists('user', array('id' => $value))) { return new lang_string('invaliddata', 'error'); } } return true; } /** * Can the current user comment on a user's plan? * * @param int $planuserid The user ID the plan belongs to. * @return bool */ public static function can_comment_user($planuserid) { global $USER; $capabilities = array('moodle/competency:plancomment'); if ($USER->id == $planuserid) { $capabilities[] = 'moodle/competency:plancommentown'; } return has_any_capability($capabilities, context_user::instance($planuserid)); } /** * Can the current user manage a user's plan? * * @param int $planuserid The user to whom the plan would belong. * @return bool */ public static function can_manage_user($planuserid) { global $USER; $context = context_user::instance($planuserid); $capabilities = array('moodle/competency:planmanage'); if ($context->instanceid == $USER->id) { $capabilities[] = 'moodle/competency:planmanageown'; } return has_any_capability($capabilities, $context); } /** * Can the current user manage a user's draft plan? * * @param int $planuserid The user to whom the plan would belong. * @return bool */ public static function can_manage_user_draft($planuserid) { global $USER; $context = context_user::instance($planuserid); $capabilities = array('moodle/competency:planmanagedraft'); if ($context->instanceid == $USER->id) { $capabilities[] = 'moodle/competency:planmanageowndraft'; } return has_any_capability($capabilities, $context); } /** * Can the current user read the comments on a user's plan? * * @param int $planuserid The user ID the plan belongs to. * @return bool */ public static function can_read_comments_user($planuserid) { // Everyone who can read the plan can read the comments. return static::can_read_user($planuserid); } /** * Can the current user view a user's plan? * * @param int $planuserid The user to whom the plan would belong. * @return bool */ public static function can_read_user($planuserid) { global $USER; $context = context_user::instance($planuserid); $capabilities = array('moodle/competency:planview'); if ($context->instanceid == $USER->id) { $capabilities[] = 'moodle/competency:planviewown'; } return has_any_capability($capabilities, $context) || self::can_manage_user($planuserid); } /** * Can the current user view a user's draft plan? * * @param int $planuserid The user to whom the plan would belong. * @return bool */ public static function can_read_user_draft($planuserid) { global $USER; $context = context_user::instance($planuserid); $capabilities = array('moodle/competency:planviewdraft'); if ($context->instanceid == $USER->id) { $capabilities[] = 'moodle/competency:planviewowndraft'; } return has_any_capability($capabilities, $context) || self::can_manage_user_draft($planuserid); } /** * Can the current user request the draft to be reviewed. * * @param int $planuserid The user to whom the plan would belong. * @return bool */ public static function can_request_review_user($planuserid) { global $USER; $capabilities = array('moodle/competency:planrequestreview'); if ($USER->id == $planuserid) { $capabilities[] = 'moodle/competency:planrequestreviewown'; } return has_any_capability($capabilities, context_user::instance($planuserid)); } /** * Can the current user review the plan. * * This means being able to send the plan from draft to active, and vice versa. * * @param int $planuserid The user to whom the plan would belong. * @return bool */ public static function can_review_user($planuserid) { return has_capability('moodle/competency:planreview', context_user::instance($planuserid)) || self::can_manage_user($planuserid); } /** * Get the plans of a user containing a specific competency. * * @param int $userid The user ID. * @param int $competencyid The competency ID. * @return plans[] */ public static function get_by_user_and_competency($userid, $competencyid) { global $DB; $sql = 'SELECT p.* FROM {' . self::TABLE . '} p LEFT JOIN {' . plan_competency::TABLE . '} pc ON pc.planid = p.id AND pc.competencyid = :competencyid1 LEFT JOIN {' . user_competency_plan::TABLE . '} ucp ON ucp.planid = p.id AND ucp.competencyid = :competencyid2 LEFT JOIN {' . template_competency::TABLE . '} tc ON tc.templateid = p.templateid AND tc.competencyid = :competencyid3 WHERE p.userid = :userid AND (pc.id IS NOT NULL OR ucp.id IS NOT NULL OR tc.id IS NOT NULL) ORDER BY p.id ASC'; $params = array( 'competencyid1' => $competencyid, 'competencyid2' => $competencyid, 'competencyid3' => $competencyid, 'userid' => $userid ); $plans = array(); $records = $DB->get_records_sql($sql, $params); foreach ($records as $record) { $plans[$record->id] = new plan(0, $record); } return $plans; } /** * Get the list of draft statuses. * * @return array Contains the status constants. */ public static function get_draft_statuses() { return array(self::STATUS_DRAFT, self::STATUS_WAITING_FOR_REVIEW, self::STATUS_IN_REVIEW); } /** * Get the recordset of the plans that are due, incomplete and not draft. * * @return \moodle_recordset */ public static function get_recordset_for_due_and_incomplete() { global $DB; $sql = "duedate > 0 AND duedate < :now AND status = :status"; $params = array('now' => time(), 'status' => self::STATUS_ACTIVE); return $DB->get_recordset_select(self::TABLE, $sql, $params); } /** * Return a list of status depending on capabilities. * * @param int $userid The user to whom the plan would belong. * @return array */ public static function get_status_list($userid) { $status = array(); if (self::can_manage_user_draft($userid)) { $status[self::STATUS_DRAFT] = get_string('planstatusdraft', 'core_competency'); } if (self::can_manage_user($userid)) { $status[self::STATUS_ACTIVE] = get_string('planstatusactive', 'core_competency'); } return $status; } /** * Update from template. * * Bulk update a lot of plans from a template * * @param template $template * @return bool */ public static function update_multiple_from_template(template $template) { global $DB; if (!$template->is_valid()) { // As we will bypass this model's validation we rely on the template being validated. throw new \coding_exception('The template must be validated before updating plans.'); } $params = array( 'templateid' => $template->get('id'), 'status' => self::STATUS_COMPLETE, 'name' => $template->get('shortname'), 'description' => $template->get('description'), 'descriptionformat' => $template->get('descriptionformat'), 'duedate' => $template->get('duedate'), ); $sql = "UPDATE {" . self::TABLE . "} SET name = :name, description = :description, descriptionformat = :descriptionformat, duedate = :duedate WHERE templateid = :templateid AND status != :status"; return $DB->execute($sql, $params); } /** * Check if a template is associated to the plan. * * @return bool */ public function is_based_on_template() { return $this->get('templateid') !== null; } /** * Check if plan can be edited. * * @return bool */ public function can_be_edited() { return !$this->is_based_on_template() && $this->get('status') != self::STATUS_COMPLETE && $this->can_manage(); } /** * Validate the due date. * When setting a due date it must not exceed the DUEDATE_THRESHOLD. * * @param int $value The due date. * @return bool|lang_string */ protected function validate_duedate($value) { // We do not check duedate when plan is draft, complete, unset, or based on a template. if ($this->is_based_on_template() || $this->is_draft() || $this->get('status') == self::STATUS_COMPLETE || empty($value)) { return true; } // During update. if ($this->get('id')) { $before = $this->beforeupdate->get('duedate'); $beforestatus = $this->beforeupdate->get('status'); // The value has not changed, then it's always OK. Though if we're going // from draft to active it has to has to be validated. if ($before == $value && !in_array($beforestatus, self::get_draft_statuses())) { return true; } } if ($value <= time()) { // We cannot set the date in the past. return new lang_string('errorcannotsetduedateinthepast', 'core_competency'); } if ($value <= time() + self::DUEDATE_THRESHOLD) { // We cannot set the date too soon, but we can leave it empty. return new lang_string('errorcannotsetduedatetoosoon', 'core_competency'); } return true; } /** * Checks if a template has user plan records. * * @param int $templateid The template ID * @return boolean */ public static function has_records_for_template($templateid) { return self::record_exists_select('templateid = ?', array($templateid)); } /** * Count the number of plans for a template, optionally filtering by status. * * @param int $templateid The template ID * @param int $status The plan status. 0 means all statuses. * @return int */ public static function count_records_for_template($templateid, $status) { $filters = array('templateid' => $templateid); if ($status > 0) { $filters['status'] = $status; } return self::count_records($filters); } /** * Get the plans for a template, optionally filtering by status. * * @param int $templateid The template ID * @param int $status The plan status. 0 means all statuses. * @param int $skip The number of plans to skip * @param int $limit The max number of plans to return * @return int */ public static function get_records_for_template($templateid, $status = 0, $skip = 0, $limit = 100) { $filters = array('templateid' => $templateid); if ($status > 0) { $filters['status'] = $status; } return self::get_records($filters, $skip, $limit); } } course_module_competency.php 0000644 00000025003 15151776373 0012367 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing competencies from the DB. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use stdClass; use lang_string; /** * Class for loading/storing course_module_competencies from the DB. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_competency extends persistent { const TABLE = 'competency_modulecomp'; /** Course competency ruleoutcome constant. */ const OUTCOME_NONE = 0; /** Course competency ruleoutcome constant. */ const OUTCOME_EVIDENCE = 1; /** Course competency ruleoutcome constant. */ const OUTCOME_RECOMMEND = 2; /** Course competency ruleoutcome constant. */ const OUTCOME_COMPLETE = 3; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'cmid' => array( 'type' => PARAM_INT ), 'competencyid' => array( 'type' => PARAM_INT ), 'sortorder' => array( 'type' => PARAM_INT ), 'ruleoutcome' => array( 'choices' => array(self::OUTCOME_NONE, self::OUTCOME_EVIDENCE, self::OUTCOME_RECOMMEND, self::OUTCOME_COMPLETE ), 'default' => self::OUTCOME_EVIDENCE, 'type' => PARAM_INT, ), 'overridegrade' => array( 'default' => false, 'type' => PARAM_BOOL ), ); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { if (($this->get('id') && $this->get('sortorder') === null) || !$this->get('id')) { $this->set('sortorder', $this->count_records(array('cmid' => $this->get('cmid')))); } } /** * Return a list of rules. * * @return array Indexed by outcome value. */ public static function get_ruleoutcome_list() { static $list = null; if ($list === null) { $list = array( self::OUTCOME_NONE => self::get_ruleoutcome_name(self::OUTCOME_NONE), self::OUTCOME_EVIDENCE => self::get_ruleoutcome_name(self::OUTCOME_EVIDENCE), self::OUTCOME_RECOMMEND => self::get_ruleoutcome_name(self::OUTCOME_RECOMMEND), self::OUTCOME_COMPLETE => self::get_ruleoutcome_name(self::OUTCOME_COMPLETE)); } return $list; } /** * Human readable rule name. * * @param int $ruleoutcome The value of ruleoutcome. * @return lang_string */ public static function get_ruleoutcome_name($ruleoutcome) { switch ($ruleoutcome) { case self::OUTCOME_NONE: $strname = 'none'; break; case self::OUTCOME_EVIDENCE: $strname = 'evidence'; break; case self::OUTCOME_RECOMMEND: $strname = 'recommend'; break; case self::OUTCOME_COMPLETE: $strname = 'complete'; break; default: throw new \moodle_exception('errorcompetencyrule', 'core_competency', '', $ruleoutcome); break; } return new lang_string('coursemodulecompetencyoutcome_' . $strname, 'core_competency'); } /** * Validate cmid ID. * * @param int $data The CM ID. * @return true|lang_string */ protected function validate_cmid($data) { global $DB; if (!$DB->record_exists('course_modules', array('id' => $data))) { return new lang_string('invalidmodule', 'error'); } return true; } /** * Validate competency ID. * * @param int $data The competency ID. * @return true|lang_string */ protected function validate_competencyid($data) { if (!competency::record_exists($data)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Return the module IDs and visible flags that include this competency in a single course. * * @param int $competencyid The competency id * @param int $courseid The course ID. * @return array of ints (cmids) */ public static function list_course_modules($competencyid, $courseid) { global $DB; $results = $DB->get_records_sql('SELECT coursemodules.id as id FROM {' . self::TABLE . '} modcomp JOIN {course_modules} coursemodules ON modcomp.cmid = coursemodules.id WHERE modcomp.competencyid = ? AND coursemodules.course = ?', array($competencyid, $courseid)); return array_keys($results); } /** * Count the competencies in this course module. * * @param int $cmid The course module id. * @return int */ public static function count_competencies($cmid) { global $DB; $sql = 'SELECT COUNT(comp.id) FROM {' . self::TABLE . '} coursemodulecomp JOIN {' . competency::TABLE . '} comp ON coursemodulecomp.competencyid = comp.id WHERE coursemodulecomp.cmid = ? '; $params = array($cmid); $results = $DB->count_records_sql($sql, $params); return $results; } /** * List the competencies in this course module. * * @param int $cmid The course module id * @return competency[] Indexed by competency ID. */ public static function list_competencies($cmid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} coursemodulecomp ON coursemodulecomp.competencyid = comp.id WHERE coursemodulecomp.cmid = ? ORDER BY coursemodulecomp.sortorder ASC'; $params = array($cmid); $results = $DB->get_recordset_sql($sql, $params); $instances = array(); foreach ($results as $result) { $comp = new competency(0, $result); $instances[$comp->get('id')] = $comp; } $results->close(); return $instances; } /** * Get a single competency from the course module (only if it is really in the course module). * * @param int $cmid The course module id * @param int $competencyid The competency id * @return competency */ public static function get_competency($cmid, $competencyid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} crsmodcomp ON crsmodcomp.competencyid = comp.id WHERE crsmodcomp.cmid = ? AND crsmodcomp.competencyid = ?'; $params = array($cmid, $competencyid); $result = $DB->get_record_sql($sql, $params); if (!$result) { throw new \coding_exception('The competency does not belong to this course module: ' . $competencyid . ', ' . $cmid); } return new competency(0, $result); } /** * Hook to execute after delete. * * @param bool $result Whether or not the delete was successful. * @return void */ protected function after_delete($result) { global $DB; if (!$result) { return; } $table = '{' . self::TABLE . '}'; $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE cmid = ? AND sortorder > ?"; $DB->execute($sql, array($this->get('cmid'), $this->get('sortorder'))); } /** * List the course_module_competencies in this course module. * * @param int $cmid The course module id * @return course_module_competency[] */ public static function list_course_module_competencies($cmid) { global $DB; $sql = 'SELECT coursemodcomp.* FROM {' . self::TABLE . '} coursemodcomp JOIN {' . competency::TABLE . '} comp ON coursemodcomp.competencyid = comp.id WHERE coursemodcomp.cmid = ? ORDER BY coursemodcomp.sortorder ASC'; $params = array($cmid); $results = $DB->get_recordset_sql($sql, $params); $instances = array(); foreach ($results as $result) { array_push($instances, new course_module_competency(0, $result)); } $results->close(); return $instances; } /** * List the relationship objects for a competency in a course. * * @param int $competencyid The competency ID. * @param int $courseid The course ID. * @return course_module_competency[] */ public static function get_records_by_competencyid_in_course($competencyid, $courseid) { global $DB; $sql = 'SELECT cmc.* FROM {' . self::TABLE . '} cmc JOIN {course_modules} cm ON cm.course = ? AND cmc.cmid = cm.id WHERE cmc.competencyid = ? ORDER BY cmc.sortorder ASC'; $params = array($courseid, $competencyid); $results = $DB->get_recordset_sql($sql, $params); $instances = array(); foreach ($results as $result) { $instances[$result->id] = new course_module_competency(0, $result); } $results->close(); return $instances; } } competency_framework.php 0000644 00000037456 15151776373 0011536 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing competency frameworks from the DB. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use coding_exception; use context; use lang_string; use stdClass; require_once($CFG->libdir . '/grade/grade_scale.php'); /** * Class for loading/storing competency frameworks from the DB. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class competency_framework extends persistent { const TABLE = 'competency_framework'; /** Taxonomy constant. */ const TAXONOMY_BEHAVIOUR = 'behaviour'; /** Taxonomy constant. */ const TAXONOMY_COMPETENCY = 'competency'; /** Taxonomy constant. */ const TAXONOMY_CONCEPT = 'concept'; /** Taxonomy constant. */ const TAXONOMY_DOMAIN = 'domain'; /** Taxonomy constant. */ const TAXONOMY_INDICATOR = 'indicator'; /** Taxonomy constant. */ const TAXONOMY_LEVEL = 'level'; /** Taxonomy constant. */ const TAXONOMY_OUTCOME = 'outcome'; /** Taxonomy constant. */ const TAXONOMY_PRACTICE = 'practice'; /** Taxonomy constant. */ const TAXONOMY_PROFICIENCY = 'proficiency'; /** Taxonomy constant. */ const TAXONOMY_SKILL = 'skill'; /** Taxonomy constant. */ const TAXONOMY_VALUE = 'value'; /** @var static The object before it was updated. */ protected $beforeupdate; /** * Get the context. * * @return context The context */ public function get_context() { return context::instance_by_id($this->get('contextid')); } /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'shortname' => array( 'type' => PARAM_TEXT ), 'idnumber' => array( 'type' => PARAM_RAW ), 'description' => array( 'type' => PARAM_CLEANHTML, 'default' => '' ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML ), 'visible' => array( 'type' => PARAM_BOOL, 'default' => 1 ), 'scaleid' => array( 'type' => PARAM_INT ), 'scaleconfiguration' => array( 'type' => PARAM_RAW ), 'contextid' => array( 'type' => PARAM_INT ), 'taxonomies' => array( 'type' => PARAM_RAW, 'default' => '' ) ); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { $this->beforeupdate = null; // During update. if ($this->get('id')) { $this->beforeupdate = new competency_framework($this->get('id')); } } /** * Return the current depth of a competency framework. * * @see competency::get_framework_depth() * @return int */ public function get_depth() { return competency::get_framework_depth($this->get('id')); } /** * Return the scale. * * @return \grade_scale */ public function get_scale() { $scale = \grade_scale::fetch(array('id' => $this->get('scaleid'))); $scale->load_items(); return $scale; } /** * Get the constant name for a level. * * @param int $level The level of the term. * @return string */ public function get_taxonomy($level) { $taxonomies = $this->get_taxonomies(); if (empty($taxonomies[$level])) { // If for some reason we cannot find the level, we fallback onto competency. $constant = self::TAXONOMY_COMPETENCY; } else { $constant = $taxonomies[$level]; } return $constant; } /** * Return the taxonomy constants indexed by level. * * @return array Contains the list of taxonomy constants indexed by level. */ protected function get_taxonomies() { $taxonomies = explode(',', $this->raw_get('taxonomies')); // Indexing first level at 1. array_unshift($taxonomies, null); unset($taxonomies[0]); // Ensure that we do not return empty levels. foreach ($taxonomies as $i => $taxonomy) { if (empty($taxonomy)) { $taxonomies[$i] = self::TAXONOMY_COMPETENCY; } } return $taxonomies; } /** * Returns true when some competencies of the framework have user competencies. * * This is useful to determine if the framework, or part of it, should be locked down. * * @return boolean */ public function has_user_competencies() { return user_competency::has_records_for_framework($this->get('id')) || user_competency_plan::has_records_for_framework($this->get('id')); } /** * Convenience method to set taxonomies from an array or string. * * @param string|array $taxonomies A string, or an array where the values are the term constants. */ protected function set_taxonomies($taxonomies) { if (is_array($taxonomies)) { $taxonomies = implode(',', $taxonomies); } $this->raw_set('taxonomies', $taxonomies); } /** * Validate the context ID. * * @param int $value The context ID. * @return bool|lang_string */ protected function validate_contextid($value) { global $DB; $context = context::instance_by_id($value, IGNORE_MISSING); if (!$context) { return new lang_string('invalidcontext', 'error'); } else if ($context->contextlevel != CONTEXT_SYSTEM && $context->contextlevel != CONTEXT_COURSECAT) { return new lang_string('invalidcontext', 'error'); } // During update. if ($this->get('id')) { // The context must never change. $oldcontextid = $DB->get_field(self::TABLE, 'contextid', array('id' => $this->get('id')), MUST_EXIST); if ($this->get('contextid') != $oldcontextid) { return new lang_string('invalidcontext', 'error'); } } return true; } /** * Validate the id number. * * @param string $value The id number. * @return bool|lang_string */ protected function validate_idnumber($value) { global $DB; $params = array( 'id' => $this->get('id'), 'idnumber' => $value, ); if ($DB->record_exists_select(self::TABLE, 'idnumber = :idnumber AND id <> :id', $params)) { return new lang_string('idnumbertaken', 'error'); } return true; } /** * Validate the scale ID. * * @param string $value The scale ID. * @return bool|lang_string */ protected function validate_scaleid($value) { global $DB; // Always validate that the scale exists. if (!$DB->record_exists_select('scale', 'id = :id', array('id' => $value))) { return new lang_string('invalidscaleid', 'error'); } // During update. if ($this->get('id')) { // Validate that we can only change the scale when it is not used yet. if ($this->beforeupdate->get('scaleid') != $value) { if ($this->beforeupdate->has_user_competencies()) { return new lang_string('errorscalealreadyused', 'core_competency'); } } } return true; } /** * Validate the scale configuration. * * @param string $value The scale configuration. * @return bool|lang_string */ protected function validate_scaleconfiguration($value) { $scaledefaultselected = false; $proficientselected = false; $scaleconfigurations = json_decode($value); if (is_array($scaleconfigurations)) { // The first element of the array contains the scale ID. $scaleinfo = array_shift($scaleconfigurations); if (empty($scaleinfo) || !isset($scaleinfo->scaleid) || $scaleinfo->scaleid != $this->get('scaleid')) { // This should never happen. return new lang_string('errorscaleconfiguration', 'core_competency'); } // Walk through the array to find proficient and default values. foreach ($scaleconfigurations as $scaleconfiguration) { if (isset($scaleconfiguration->scaledefault) && $scaleconfiguration->scaledefault) { $scaledefaultselected = true; } if (isset($scaleconfiguration->proficient) && $scaleconfiguration->proficient) { $proficientselected = true; } } } if (!$scaledefaultselected || !$proficientselected) { return new lang_string('errorscaleconfiguration', 'core_competency'); } return true; } /** * Validate taxonomies. * * @param mixed $value The taxonomies. * @return true|lang_string */ protected function validate_taxonomies($value) { $terms = explode(',', $value); foreach ($terms as $term) { if (!empty($term) && !array_key_exists($term, self::get_taxonomies_list())) { return new lang_string('invalidtaxonomy', 'core_competency', $term); } } return true; } /** * Extract the default grade from a scale configuration. * * Returns an array where the first element is the grade, and the second * is a boolean representing whether or not this grade is considered 'proficient'. * * @param string $config JSON encoded config. * @return array(int grade, int proficient) */ public static function get_default_grade_from_scale_configuration($config) { $config = json_decode($config); if (!is_array($config)) { throw new coding_exception('Unexpected scale configuration.'); } // Remove the scale ID from the config. array_shift($config); foreach ($config as $part) { if ($part->scaledefault) { return array((int) $part->id, (int) $part->proficient); } } throw new coding_exception('Invalid scale configuration, default not found.'); } /** * Extract the proficiency of a grade from a scale configuration. * * @param string $config JSON encoded config. * @param int $grade The grade. * @return int Representing a boolean */ public static function get_proficiency_of_grade_from_scale_configuration($config, $grade) { $config = json_decode($config); if (!is_array($config)) { throw new coding_exception('Unexpected scale configuration.'); } // Remove the scale ID from the config. array_shift($config); foreach ($config as $part) { if ($part->id == $grade) { return (int) $part->proficient; } } return 0; } /** * Get the string of a taxonomy from a constant * * @param string $constant The taxonomy constant. * @return lang_string */ public static function get_taxonomy_from_constant($constant) { return self::get_taxonomies_list()[$constant]; } /** * Get the list of all taxonomies. * * @return array Where the key is the taxonomy constant, and the value its translation. */ public static function get_taxonomies_list() { static $list = null; // At some point we'll have to switch to not using static cache, mainly for Unit Tests in case we // decide to allow more taxonomies to be added dynamically from a CFG variable for instance. if ($list === null) { $list = array( self::TAXONOMY_BEHAVIOUR => new lang_string('taxonomy_' . self::TAXONOMY_BEHAVIOUR, 'core_competency'), self::TAXONOMY_COMPETENCY => new lang_string('taxonomy_' . self::TAXONOMY_COMPETENCY, 'core_competency'), self::TAXONOMY_CONCEPT => new lang_string('taxonomy_' . self::TAXONOMY_CONCEPT, 'core_competency'), self::TAXONOMY_DOMAIN => new lang_string('taxonomy_' . self::TAXONOMY_DOMAIN, 'core_competency'), self::TAXONOMY_INDICATOR => new lang_string('taxonomy_' . self::TAXONOMY_INDICATOR, 'core_competency'), self::TAXONOMY_LEVEL => new lang_string('taxonomy_' . self::TAXONOMY_LEVEL, 'core_competency'), self::TAXONOMY_OUTCOME => new lang_string('taxonomy_' . self::TAXONOMY_OUTCOME, 'core_competency'), self::TAXONOMY_PRACTICE => new lang_string('taxonomy_' . self::TAXONOMY_PRACTICE, 'core_competency'), self::TAXONOMY_PROFICIENCY => new lang_string('taxonomy_' . self::TAXONOMY_PROFICIENCY, 'core_competency'), self::TAXONOMY_SKILL => new lang_string('taxonomy_' . self::TAXONOMY_SKILL, 'core_competency'), self::TAXONOMY_VALUE => new lang_string('taxonomy_' . self::TAXONOMY_VALUE, 'core_competency'), ); } return $list; } /** * Get a uniq idnumber. * * @param string $idnumber the framework idnumber * @return string */ public static function get_unused_idnumber($idnumber) { global $DB; $currentidnumber = $idnumber; $counter = 0; // Iteratere while the idnumber exists. while ($DB->record_exists_select(static::TABLE, 'idnumber = ?', array($currentidnumber))) { $suffixidnumber = '_' . ++$counter; $currentidnumber = substr($idnumber, 0, 100 - strlen($suffixidnumber)).$suffixidnumber; } // Return the uniq idnumber. return $currentidnumber; } /** * Whether or not the current user can manage the framework. * * @return bool */ public function can_manage() { return self::can_manage_context($this->get_context()); } /** * Whether or not the current user can manage the framework. * * @param context $context * @return bool */ public static function can_manage_context($context) { return has_capability('moodle/competency:competencymanage', $context); } /** * Whether or not the current user can read the framework. * * @return bool */ public function can_read() { return self::can_read_context($this->get_context()); } /** * Whether or not the current user can read the framework. * * @param context $context * @return bool */ public static function can_read_context($context) { return has_capability('moodle/competency:competencyview', $context) || self::can_manage_context($context); } } api.php 0000644 00000145140 15151776373 0006052 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing helper methods for processing data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use coding_exception; use context_helper; use context_system; use core\invalid_persistent_exception; use core\message\message; use core\task\manager; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\contextlist_collection; use core_user; use dml_exception; use moodle_exception; use moodle_url; use required_capability_exception; use stdClass; use tool_dataprivacy\external\data_request_exporter; use tool_dataprivacy\local\helper; use tool_dataprivacy\task\process_data_request_task; use tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); /** * Class containing helper methods for processing data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** Data export request type. */ const DATAREQUEST_TYPE_EXPORT = 1; /** Data deletion request type. */ const DATAREQUEST_TYPE_DELETE = 2; /** Other request type. Usually of enquiries to the DPO. */ const DATAREQUEST_TYPE_OTHERS = 3; /** Newly submitted and we haven't yet started finding out where they have data. */ const DATAREQUEST_STATUS_PENDING = 0; /** Metadata ready and awaiting review and approval by the Data Protection officer. */ const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2; /** Request approved and will be processed soon. */ const DATAREQUEST_STATUS_APPROVED = 3; /** The request is now being processed. */ const DATAREQUEST_STATUS_PROCESSING = 4; /** Information/other request completed. */ const DATAREQUEST_STATUS_COMPLETE = 5; /** Data request cancelled by the user. */ const DATAREQUEST_STATUS_CANCELLED = 6; /** Data request rejected by the DPO. */ const DATAREQUEST_STATUS_REJECTED = 7; /** Data request download ready. */ const DATAREQUEST_STATUS_DOWNLOAD_READY = 8; /** Data request expired. */ const DATAREQUEST_STATUS_EXPIRED = 9; /** Data delete request completed, account is removed. */ const DATAREQUEST_STATUS_DELETED = 10; /** Approve data request. */ const DATAREQUEST_ACTION_APPROVE = 1; /** Reject data request. */ const DATAREQUEST_ACTION_REJECT = 2; /** * Determines whether the user can contact the site's Data Protection Officer via Moodle. * * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled. * @throws dml_exception */ public static function can_contact_dpo() { return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1; } /** * Checks whether the current user has the capability to manage data requests. * * @param int $userid The user ID. * @return bool */ public static function can_manage_data_requests($userid) { // Privacy officers can manage data requests. return self::is_site_dpo($userid); } /** * Checks if the current user can manage the data registry at the provided id. * * @param int $contextid Fallback to system context id. * @throws \required_capability_exception * @return null */ public static function check_can_manage_data_registry($contextid = false) { if ($contextid) { $context = \context_helper::instance_by_id($contextid); } else { $context = \context_system::instance(); } require_capability('tool/dataprivacy:managedataregistry', $context); } /** * Fetches the list of configured privacy officer roles. * * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes * any role that doesn't have the required capability anymore. * * @return int[] * @throws dml_exception */ public static function get_assigned_privacy_officer_roles() { $roleids = []; // Get roles from config. $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles'))); if (!empty($configroleids)) { // Fetch roles that have the capability to manage data requests. $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests')); // Extract the configured roles that have the capability from the list of capable roles. $roleids = array_intersect($capableroles, $configroleids); } return $roleids; } /** * Fetches the role shortnames of Data Protection Officer roles. * * @return array An array of the DPO role shortnames */ public static function get_dpo_role_names() : array { global $DB; $dporoleids = self::get_assigned_privacy_officer_roles(); $dponames = array(); if (!empty($dporoleids)) { list($insql, $inparams) = $DB->get_in_or_equal($dporoleids); $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams); } return $dponames; } /** * Fetches the list of users with the Privacy Officer role. */ public static function get_site_dpos() { // Get role(s) that can manage data requests. $dporoles = self::get_assigned_privacy_officer_roles(); $dpos = []; $context = context_system::instance(); foreach ($dporoles as $roleid) { $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' . 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '. 'u.country, u.picture, u.idnumber, u.department, u.institution, '. 'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' . 'r.name AS rolename, r.sortorder, '. 'r.shortname AS roleshortname, rn.name AS rolecoursealias'; // Fetch users that can manage data requests. $dpos += get_role_users($roleid, $context, false, $fields); } // If the site has no data protection officer, defer to site admin(s). if (empty($dpos)) { $dpos = get_admins(); } return $dpos; } /** * Checks whether a given user is a site Privacy Officer. * * @param int $userid The user ID. * @return bool */ public static function is_site_dpo($userid) { $dpos = self::get_site_dpos(); return array_key_exists($userid, $dpos) || is_siteadmin(); } /** * Lodges a data request and sends the request details to the site Data Protection Officer(s). * * @param int $foruser The user whom the request is being made for. * @param int $type The request type. * @param string $comments Request comments. * @param int $creationmethod The creation method of the data request. * @param bool $notify Notify DPOs of this pending request. * @return data_request * @throws invalid_persistent_exception * @throws coding_exception */ public static function create_data_request($foruser, $type, $comments = '', $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL, $notify = null ) { global $USER; if (null === $notify) { // Only if notifications have not been decided by caller. if ( data_request::DATAREQUEST_CREATION_AUTO == $creationmethod) { // If the request was automatically created, then do not notify unless explicitly set. $notify = false; } else { $notify = true; } } $datarequest = new data_request(); // The user the request is being made for. $datarequest->set('userid', $foruser); // The cron is considered to be a guest user when it creates a data request. // NOTE: This should probably be changed. We should leave the default value for $requestinguser if // the request is not explicitly created by a specific user. $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ? get_admin()->id : $USER->id; // The user making the request. $datarequest->set('requestedby', $requestinguser); // Set status. $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL; if (self::is_automatic_request_approval_on($type)) { // Set status to approved if automatic data request approval is enabled. $status = self::DATAREQUEST_STATUS_APPROVED; // Set the privacy officer field if the one making the data request is a privacy officer. if (self::is_site_dpo($requestinguser)) { $datarequest->set('dpo', $requestinguser); } // Mark this request as system approved. $datarequest->set('systemapproved', true); // No need to notify privacy officer(s) about automatically approved data requests. $notify = false; } $datarequest->set('status', $status); // Set request type. $datarequest->set('type', $type); // Set request comments. $datarequest->set('comments', $comments); // Set the creation method. $datarequest->set('creationmethod', $creationmethod); // Store subject access request. $datarequest->create(); // Queue the ad-hoc task for automatically approved data requests. if ($status == self::DATAREQUEST_STATUS_APPROVED) { $userid = null; if ($type == self::DATAREQUEST_TYPE_EXPORT) { $userid = $foruser; } self::queue_data_request_task($datarequest->get('id'), $userid); } if ($notify) { // Get the list of the site Data Protection Officers. $dpos = self::get_site_dpos(); // Email the data request to the Data Protection Officer(s)/Admin(s). foreach ($dpos as $dpo) { self::notify_dpo($dpo, $datarequest); } } return $datarequest; } /** * Fetches the list of the data requests. * * If user ID is provided, it fetches the data requests for the user. * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests. * (e.g. Users with the Data Protection Officer roles) * * @param int $userid The User ID. * @param int[] $statuses The status filters. * @param int[] $types The request type filters. * @param int[] $creationmethods The request creation method filters. * @param string $sort The order by clause. * @param int $offset Amount of records to skip. * @param int $limit Amount of records to fetch. * @return data_request[] * @throws coding_exception * @throws dml_exception */ public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [], $sort = '', $offset = 0, $limit = 0) { global $DB, $USER; $results = []; $sqlparams = []; $sqlconditions = []; // Set default sort. if (empty($sort)) { $sort = 'status ASC, timemodified ASC'; } // Set status filters. if (!empty($statuses)) { list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED); $sqlconditions[] = "status $statusinsql"; } // Set request type filter. if (!empty($types)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED); $sqlconditions[] = "type $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } // Set request creation method filter. if (!empty($creationmethods)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED); $sqlconditions[] = "creationmethod $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } if ($userid) { // Get the data requests for the user or data requests made by the user. $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)"; $params = [ 'userid' => $userid, 'requestedby' => $userid ]; // Build a list of user IDs that the user is allowed to make data requests for. // Of course, the user should be included in this list. $alloweduserids = [$userid]; // Get any users that the user can make data requests for. if ($children = helper::get_children_of_user($userid)) { // Get the list of user IDs of the children and merge to the allowed user IDs. $alloweduserids = array_merge($alloweduserids, array_keys($children)); } list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED); $sqlconditions[] .= "userid $insql"; $select = implode(' AND ', $sqlconditions); $params = array_merge($params, $inparams, $sqlparams); $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit); } else { // If the current user is one of the site's Data Protection Officers, then fetch all data requests. if (self::is_site_dpo($USER->id)) { if (!empty($sqlconditions)) { $select = implode(' AND ', $sqlconditions); $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit); } else { $results = data_request::get_records(null, $sort, '', $offset, $limit); } } } // If any are due to expire, expire them and re-fetch updated data. if (empty($statuses) || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses) || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) { $expiredrequests = data_request::get_expired_requests($userid); if (!empty($expiredrequests)) { data_request::expire($expiredrequests); $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit); } } return $results; } /** * Fetches the count of data request records based on the given parameters. * * @param int $userid The User ID. * @param int[] $statuses The status filters. * @param int[] $types The request type filters. * @param int[] $creationmethods The request creation method filters. * @return int * @throws coding_exception * @throws dml_exception */ public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) { global $DB, $USER; $count = 0; $sqlparams = []; $sqlconditions = []; if (!empty($statuses)) { list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED); $sqlconditions[] = "status $statusinsql"; } if (!empty($types)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED); $sqlconditions[] = "type $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } if (!empty($creationmethods)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED); $sqlconditions[] = "creationmethod $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } if ($userid) { // Get the data requests for the user or data requests made by the user. $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)"; $params = [ 'userid' => $userid, 'requestedby' => $userid ]; // Build a list of user IDs that the user is allowed to make data requests for. // Of course, the user should be included in this list. $alloweduserids = [$userid]; // Get any users that the user can make data requests for. if ($children = helper::get_children_of_user($userid)) { // Get the list of user IDs of the children and merge to the allowed user IDs. $alloweduserids = array_merge($alloweduserids, array_keys($children)); } list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED); $sqlconditions[] .= "userid $insql"; $select = implode(' AND ', $sqlconditions); $params = array_merge($params, $inparams, $sqlparams); $count = data_request::count_records_select($select, $params); } else { // If the current user is one of the site's Data Protection Officers, then fetch all data requests. if (self::is_site_dpo($USER->id)) { if (!empty($sqlconditions)) { $select = implode(' AND ', $sqlconditions); $count = data_request::count_records_select($select, $sqlparams); } else { $count = data_request::count_records(); } } } return $count; } /** * Checks whether there is already an existing pending/in-progress data request for a user for a given request type. * * @param int $userid The user ID. * @param int $type The request type. * @return bool * @throws coding_exception * @throws dml_exception */ public static function has_ongoing_request($userid, $type) { global $DB; // Check if the user already has an incomplete data request of the same type. $nonpendingstatuses = [ self::DATAREQUEST_STATUS_COMPLETE, self::DATAREQUEST_STATUS_CANCELLED, self::DATAREQUEST_STATUS_REJECTED, self::DATAREQUEST_STATUS_DOWNLOAD_READY, self::DATAREQUEST_STATUS_EXPIRED, self::DATAREQUEST_STATUS_DELETED, ]; list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false); $select = "type = :type AND userid = :userid AND status {$insql}"; $params = array_merge([ 'type' => $type, 'userid' => $userid ], $inparams); return data_request::record_exists_select($select, $params); } /** * Find whether any ongoing requests exist for a set of users. * * @param array $userids * @return array */ public static function find_ongoing_request_types_for_users(array $userids) : array { global $DB; if (empty($userids)) { return []; } // Check if the user already has an incomplete data request of the same type. $nonpendingstatuses = [ self::DATAREQUEST_STATUS_COMPLETE, self::DATAREQUEST_STATUS_CANCELLED, self::DATAREQUEST_STATUS_REJECTED, self::DATAREQUEST_STATUS_DOWNLOAD_READY, self::DATAREQUEST_STATUS_EXPIRED, self::DATAREQUEST_STATUS_DELETED, ]; list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false); list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us'); $select = "userid {$userinsql} AND status {$statusinsql}"; $params = array_merge($statusparams, $userparams); $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type'); $returnval = []; foreach ($userids as $userid) { $returnval[$userid] = (object) []; } foreach ($requests as $request) { $returnval[$request->userid]->{$request->type} = true; } return $returnval; } /** * Determines whether a request is active or not based on its status. * * @param int $status The request status. * @return bool */ public static function is_active($status) { // List of statuses which doesn't require any further processing. $finalstatuses = [ self::DATAREQUEST_STATUS_COMPLETE, self::DATAREQUEST_STATUS_CANCELLED, self::DATAREQUEST_STATUS_REJECTED, self::DATAREQUEST_STATUS_DOWNLOAD_READY, self::DATAREQUEST_STATUS_EXPIRED, self::DATAREQUEST_STATUS_DELETED, ]; return !in_array($status, $finalstatuses); } /** * Cancels the data request for a given request ID. * * @param int $requestid The request identifier. * @param int $status The request status. * @param int $dpoid The user ID of the Data Protection Officer * @param string $comment The comment about the status update. * @return bool * @throws invalid_persistent_exception * @throws coding_exception */ public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') { // Update the request. $datarequest = new data_request($requestid); $datarequest->set('status', $status); if ($dpoid) { $datarequest->set('dpo', $dpoid); } // Update the comment if necessary. if (!empty(trim($comment))) { $params = [ 'date' => userdate(time()), 'comment' => $comment ]; $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params); // Check if there's an existing DPO comment. $currentcomment = trim($datarequest->get('dpocomment')); if ($currentcomment) { // Append the new comment to the current comment and give them 1 line space in between. $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave; } $datarequest->set('dpocomment', $commenttosave); } return $datarequest->update(); } /** * Fetches a request based on the request ID. * * @param int $requestid The request identifier * @return data_request */ public static function get_request($requestid) { return new data_request($requestid); } /** * Approves a data request based on the request ID. * * @param int $requestid The request identifier * @return bool * @throws coding_exception * @throws dml_exception * @throws invalid_persistent_exception * @throws required_capability_exception * @throws moodle_exception */ public static function approve_data_request($requestid) { global $USER; // Check first whether the user can manage data requests. if (!self::can_manage_data_requests($USER->id)) { $context = context_system::instance(); throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); } // Check if request is already awaiting for approval. $request = new data_request($requestid); if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); } // Check if current user has permission to approve delete data request. if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) { throw new required_capability_exception(context_system::instance(), 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', ''); } // Update the status and the DPO. $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id); // Fire an ad hoc task to initiate the data request process. $userid = null; if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) { $userid = $request->get('userid'); } self::queue_data_request_task($requestid, $userid); return $result; } /** * Rejects a data request based on the request ID. * * @param int $requestid The request identifier * @return bool * @throws coding_exception * @throws dml_exception * @throws invalid_persistent_exception * @throws required_capability_exception * @throws moodle_exception */ public static function deny_data_request($requestid) { global $USER; if (!self::can_manage_data_requests($USER->id)) { $context = context_system::instance(); throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); } // Check if request is already awaiting for approval. $request = new data_request($requestid); if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); } // Check if current user has permission to reject delete data request. if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) { throw new required_capability_exception(context_system::instance(), 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', ''); } // Update the status and the DPO. return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id); } /** * Sends a message to the site's Data Protection Officer about a request. * * @param stdClass $dpo The DPO user record * @param data_request $request The data request * @return int|false * @throws coding_exception * @throws moodle_exception */ public static function notify_dpo($dpo, data_request $request) { global $PAGE, $SITE; $output = $PAGE->get_renderer('tool_dataprivacy'); $usercontext = \context_user::instance($request->get('requestedby')); $requestexporter = new data_request_exporter($request, ['context' => $usercontext]); $requestdata = $requestexporter->export($output); // Create message to send to the Data Protection Officer(s). $typetext = null; $typetext = $requestdata->typename; $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext); $requestedby = $requestdata->requestedbyuser; $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); $message = new message(); $message->courseid = $SITE->id; $message->component = 'tool_dataprivacy'; $message->name = 'contactdataprotectionofficer'; $message->userfrom = $requestedby->id; $message->replyto = $requestedby->email; $message->replytoname = $requestedby->fullname; $message->subject = $subject; $message->fullmessageformat = FORMAT_HTML; $message->notification = 1; $message->contexturl = $datarequestsurl; $message->contexturlname = get_string('datarequests', 'tool_dataprivacy'); // Prepare the context data for the email message body. $messagetextdata = [ 'requestedby' => $requestedby->fullname, 'requesttype' => $typetext, 'requestdate' => userdate($requestdata->timecreated), 'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]), 'requestoriginurl' => new moodle_url('/'), 'requestcomments' => $requestdata->messagehtml, 'datarequestsurl' => $datarequestsurl ]; $requestingfor = $requestdata->foruser; if ($requestedby->id == $requestingfor->id) { $messagetextdata['requestfor'] = $messagetextdata['requestedby']; } else { $messagetextdata['requestfor'] = $requestingfor->fullname; } // Email the data request to the Data Protection Officer(s)/Admin(s). $messagetextdata['dponame'] = fullname($dpo); // Render message email body. $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata); $message->userto = $dpo; $message->fullmessage = html_to_text($messagehtml); $message->fullmessagehtml = $messagehtml; // Send message. return message_send($message); } /** * Checks whether a non-DPO user can make a data request for another user. * * @param int $user The user ID of the target user. * @param int $requester The user ID of the user making the request. * @return bool */ public static function can_create_data_request_for_user($user, $requester = null) { $usercontext = \context_user::instance($user); return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester); } /** * Require that the current user can make a data request for the specified other user. * * @param int $user The user ID of the target user. * @param int $requester The user ID of the user making the request. * @return bool */ public static function require_can_create_data_request_for_user($user, $requester = null) { $usercontext = \context_user::instance($user); require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester); return true; } /** * Check if user has permission to create data download request for themselves * * @param int|null $userid * @return bool */ public static function can_create_data_download_request_for_self(int $userid = null): bool { global $USER; $userid = $userid ?: $USER->id; return has_capability('tool/dataprivacy:downloadownrequest', \context_user::instance($userid), $userid); } /** * Check if user has permisson to create data deletion request for themselves. * * @param int|null $userid ID of the user. * @return bool * @throws coding_exception */ public static function can_create_data_deletion_request_for_self(int $userid = null): bool { global $USER; $userid = $userid ?: $USER->id; return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid) && !is_primary_admin($userid); } /** * Check if user has permission to create data deletion request for another user. * * @param int|null $userid ID of the user. * @return bool * @throws coding_exception * @throws dml_exception */ public static function can_create_data_deletion_request_for_other(int $userid = null): bool { global $USER; $userid = $userid ?: $USER->id; return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid); } /** * Check if parent can create data deletion request for their children. * * @param int $userid ID of a user being requested. * @param int|null $requesterid ID of a user making request. * @return bool * @throws coding_exception */ public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool { global $USER; $requesterid = $requesterid ?: $USER->id; return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid), $requesterid) && !is_primary_admin($userid); } /** * Checks whether a user can download a data request. * * @param int $userid Target user id (subject of data request) * @param int $requesterid Requester user id (person who requsted it) * @param int|null $downloaderid Person who wants to download user id (default current) * @return bool * @throws coding_exception */ public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) { global $USER; if (!$downloaderid) { $downloaderid = $USER->id; } $usercontext = \context_user::instance($userid); // If it's your own and you have the right capability, you can download it. if ($userid == $downloaderid && self::can_create_data_download_request_for_self($downloaderid)) { return true; } // If you can download anyone's in that context, you can download it. if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) { return true; } // If you can have the 'child access' ability to request in that context, and you are the one // who requested it, then you can download it. if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) { return true; } return false; } /** * Gets an action menu link to download a data request. * * @param \context_user $usercontext User context (of user who the data is for) * @param int $requestid Request id * @return \action_menu_link_secondary Action menu link * @throws coding_exception */ public static function get_download_link(\context_user $usercontext, $requestid) { $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true); $downloadtext = get_string('download', 'tool_dataprivacy'); return new \action_menu_link_secondary($downloadurl, null, $downloadtext); } /** * Creates a new data purpose. * * @param stdClass $record * @return \tool_dataprivacy\purpose. */ public static function create_purpose(stdClass $record) { $purpose = new purpose(0, $record); $purpose->create(); return $purpose; } /** * Updates an existing data purpose. * * @param stdClass $record * @return \tool_dataprivacy\purpose. */ public static function update_purpose(stdClass $record) { if (!isset($record->sensitivedatareasons)) { $record->sensitivedatareasons = ''; } $purpose = new purpose($record->id); $purpose->from_record($record); $result = $purpose->update(); return $purpose; } /** * Deletes a data purpose. * * @param int $id * @return bool */ public static function delete_purpose($id) { $purpose = new purpose($id); if ($purpose->is_used()) { throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.'); } return $purpose->delete(); } /** * Get all system data purposes. * * @return \tool_dataprivacy\purpose[] */ public static function get_purposes() { return purpose::get_records([], 'name', 'ASC'); } /** * Creates a new data category. * * @param stdClass $record * @return \tool_dataprivacy\category. */ public static function create_category(stdClass $record) { $category = new category(0, $record); $category->create(); return $category; } /** * Updates an existing data category. * * @param stdClass $record * @return \tool_dataprivacy\category. */ public static function update_category(stdClass $record) { $category = new category($record->id); $category->from_record($record); $result = $category->update(); return $category; } /** * Deletes a data category. * * @param int $id * @return bool */ public static function delete_category($id) { $category = new category($id); if ($category->is_used()) { throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.'); } return $category->delete(); } /** * Get all system data categories. * * @return \tool_dataprivacy\category[] */ public static function get_categories() { return category::get_records([], 'name', 'ASC'); } /** * Sets the context instance purpose and category. * * @param \stdClass $record * @return \tool_dataprivacy\context_instance */ public static function set_context_instance($record) { if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) { // Update. $instance->from_record($record); if (empty($record->purposeid) && empty($record->categoryid)) { // We accept one of them to be null but we delete it if both are null. self::unset_context_instance($instance); return; } } else { // Add. $instance = new context_instance(0, $record); } $instance->save(); return $instance; } /** * Unsets the context instance record. * * @param \tool_dataprivacy\context_instance $instance * @return null */ public static function unset_context_instance(context_instance $instance) { $instance->delete(); } /** * Sets the context level purpose and category. * * @throws \coding_exception * @param \stdClass $record * @return contextlevel */ public static function set_contextlevel($record) { global $DB; if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) { throw new \coding_exception('Only context system and context user can set a contextlevel ' . 'purpose and retention'); } if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) { // Update. $contextlevel->from_record($record); } else { // Add. $contextlevel = new contextlevel(0, $record); } $contextlevel->save(); // We sync with their defaults as we removed these options from the defaults page. $classname = \context_helper::get_class_for_level($record->contextlevel); list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname); set_config($purposevar, $record->purposeid, 'tool_dataprivacy'); set_config($categoryvar, $record->categoryid, 'tool_dataprivacy'); return $contextlevel; } /** * Returns the effective category given a context instance. * * @param \context $context * @param int $forcedvalue Use this categoryid value as if this was this context instance category. * @return category|false */ public static function get_effective_context_category(\context $context, $forcedvalue = false) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_context_value($context, 'category', $forcedvalue); } /** * Returns the effective purpose given a context instance. * * @param \context $context * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose. * @return purpose|false */ public static function get_effective_context_purpose(\context $context, $forcedvalue = false) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue); } /** * Returns the effective category given a context level. * * @param int $contextlevel * @return category|false */ public static function get_effective_contextlevel_category($contextlevel) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_contextlevel_value($contextlevel, 'category'); } /** * Returns the effective purpose given a context level. * * @param int $contextlevel * @param int $forcedvalue Use this purposeid value as if this was this context level purpose. * @return purpose|false */ public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue); } /** * Creates an expired context record for the provided context id. * * @param int $contextid * @return \tool_dataprivacy\expired_context */ public static function create_expired_context($contextid) { $record = (object)[ 'contextid' => $contextid, 'status' => expired_context::STATUS_EXPIRED, ]; $expiredctx = new expired_context(0, $record); $expiredctx->save(); return $expiredctx; } /** * Deletes an expired context record. * * @param int $id The tool_dataprivacy_ctxexpire id. * @return bool True on success. */ public static function delete_expired_context($id) { $expiredcontext = new expired_context($id); return $expiredcontext->delete(); } /** * Updates the status of an expired context. * * @param \tool_dataprivacy\expired_context $expiredctx * @param int $status * @return null */ public static function set_expired_context_status(expired_context $expiredctx, $status) { $expiredctx->set('status', $status); $expiredctx->save(); } /** * Finds all contextlists having at least one approved context, and returns them as in a contextlist_collection. * * @param contextlist_collection $collection The collection of unapproved contextlist objects. * @param \stdClass $foruser The target user * @param int $type The purpose of the collection * @return contextlist_collection The collection of approved_contextlist objects. */ public static function get_approved_contextlist_collection_for_collection(contextlist_collection $collection, \stdClass $foruser, int $type) : contextlist_collection { // Create the approved contextlist collection object. $approvedcollection = new contextlist_collection($collection->get_userid()); $isconfigured = data_registry::defaults_set(); foreach ($collection as $contextlist) { $contextids = []; foreach ($contextlist as $context) { if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $type) { // Data can only be deleted from it if the context is either expired, or unprotected. // Note: We can only check whether a context is expired or unprotected if the site is configured and // defaults are set appropriately. If they are not, we treat all contexts as though they are // unprotected. $purpose = static::get_effective_context_purpose($context); if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) { continue; } } $contextids[] = $context->id; } // The data for the last component contextlist won't have been written yet, so write it now. if (!empty($contextids)) { $approvedcollection->add_contextlist( new approved_contextlist($foruser, $contextlist->get_component(), $contextids) ); } } return $approvedcollection; } /** * Updates the default category and purpose for a given context level (and optionally, a plugin). * * @param int $contextlevel The context level. * @param int $categoryid The ID matching the category. * @param int $purposeid The ID matching the purpose record. * @param int $activity The name of the activity that we're making a defaults configuration for. * @param bool $override Whether to override the purpose/categories of existing instances to these defaults. * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception. */ public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) { global $DB; // Get the class name associated with this context level. $classname = context_helper::get_class_for_level($contextlevel); list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity); // Check the default category to be set. if ($categoryid == context_instance::INHERIT) { unset_config($categoryvar, 'tool_dataprivacy'); } else { // Make sure the given category ID exists first. $categorypersistent = new category($categoryid); $categorypersistent->read(); // Then set the new default value. set_config($categoryvar, $categoryid, 'tool_dataprivacy'); } // Check the default purpose to be set. if ($purposeid == context_instance::INHERIT) { // If the defaults is set to inherit, just unset the config value. unset_config($purposevar, 'tool_dataprivacy'); } else { // Make sure the given purpose ID exists first. $purposepersistent = new purpose($purposeid); $purposepersistent->read(); // Then set the new default value. set_config($purposevar, $purposeid, 'tool_dataprivacy'); } // Unset instances that have been assigned with custom purpose and category, if override was specified. if ($override) { // We'd like to find context IDs that we want to unset. $statements = ["SELECT c.id as contextid FROM {context} c"]; // Based on this context level. $params = ['contextlevel' => $contextlevel]; if ($contextlevel == CONTEXT_MODULE) { // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table. $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid"; // And that the module is listed on the modules table. $statements[] = "JOIN {modules} m ON m.id = cm.module"; if ($activity) { // If we're overriding for an activity module, make sure that the context instance matches that activity. $statements[] = "AND m.name = :modname"; $params['modname'] = $activity; } } // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table. $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id"; // And that the context level of this instance matches the given context level. $statements[] = "WHERE c.contextlevel = :contextlevel"; // Build our SQL query by gluing the statements. $sql = implode("\n", $statements); // Get the context records matching our query. $contextids = $DB->get_fieldset_sql($sql, $params); // Delete the matching context instances. foreach ($contextids as $contextid) { if ($instance = context_instance::get_record_by_contextid($contextid, false)) { self::unset_context_instance($instance); } } } return true; } /** * Format the supplied date interval as a retention period. * * @param \DateInterval $interval * @return string */ public static function format_retention_period(\DateInterval $interval) : string { // It is one or another. if ($interval->y) { $formattedtime = get_string('numyears', 'moodle', $interval->format('%y')); } else if ($interval->m) { $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m')); } else if ($interval->d) { $formattedtime = get_string('numdays', 'moodle', $interval->format('%d')); } else { $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy'); } return $formattedtime; } /** * Whether automatic data request approval is turned on or not for the given request type. * * @param int $type The request type. * @return bool */ public static function is_automatic_request_approval_on(int $type): bool { switch ($type) { case self::DATAREQUEST_TYPE_EXPORT: return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval')); case self::DATAREQUEST_TYPE_DELETE: return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval')); } return false; } /** * Creates an ad-hoc task for the data request. * * @param int $requestid The data request ID. * @param int $userid Optional. The user ID to run the task as, if necessary. */ public static function queue_data_request_task(int $requestid, int $userid = null): void { $task = new process_data_request_task(); $task->set_custom_data(['requestid' => $requestid]); if ($userid) { $task->set_userid($userid); } manager::queue_adhoc_task($task, true); } } persistent.php 0000644 00000005515 15151776373 0007502 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Abstract class for core_competency objects saved to the DB. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); // We need to alias the invalid_persistent_exception, because the persistent classes from // core_competency used to throw a \core_competency\invalid_persistent_exception. They now // fully inherit from \core\persistent which throws a core exception. Using class_alias // ensures that previous try/catch statements still work. Also note that we always need // need to alias, we cannot do it passively in the classloader because try/catch statements // do not trigger a class loading. Note that for this trick to work, all the classes // which were extending \core_competency\persistent still need to extend it or the alias // won't be effective. class_alias('core\\invalid_persistent_exception', 'core_competency\\invalid_persistent_exception'); /** * Abstract class for core_competency objects saved to the DB. * * This is a legacy class which all core_competency persistent classes created prior * to 3.3 must extend. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class persistent extends \core\persistent { /** * Magic method to capture getters and setters. * This is only available for competency persistents for backwards compatibility. * It is recommended to use get('propertyname') and set('propertyname', 'value') directly. * * @param string $method Callee. * @param array $arguments List of arguments. * @return mixed */ final public function __call($method, $arguments) { debugging('Use of magic setters and getters is deprecated. Use get() and set().', DEBUG_DEVELOPER); if (strpos($method, 'get_') === 0) { return $this->get(substr($method, 4)); } else if (strpos($method, 'set_') === 0) { return $this->set(substr($method, 4), $arguments[0]); } throw new \coding_exception('Unexpected method call: ' . $method); } } competency_rule_all.php 0000644 00000005420 15151776373 0011322 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Competency rule all. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use lang_string; /** * Competency rule all class. * * This rule is considered matched when all the children of a competency are completed. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class competency_rule_all extends competency_rule { /** * Whether or not the rule is matched. * * @param user_competency $usercompetency The user competency. * @return bool */ public function matches(user_competency $usercompetency) { global $DB; // TODO Improve performance here, perhaps the caller could already provide records. $children = competency::get_records(array('parentid' => $this->competency->get('id'))); if (empty($children)) { // Leaves are not compatible with this rule. return false; } $ids = array(); foreach ($children as $child) { $ids[] = $child->get('id'); } list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); $sql = "userid = :userid AND proficiency = :proficiency AND competencyid $insql"; $params['userid'] = $usercompetency->get('userid'); $params['proficiency'] = 1; // Is the user is marked as proficient in all children? return user_competency::count_records_select($sql, $params) === count($ids); } /** * Validate the rule config. * * @param string $value The value to validate. * @return bool */ public function validate_config($value) { return $value === null; } /** * The name of the rule. * * @return lang_string */ public static function get_name() { return new lang_string('allchildrenarecomplete', 'core_competency'); } } url.php 0000644 00000014112 15151776373 0006075 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * URL manager. * * @package core_competency * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use moodle_url; /** * URL manager class. * * This class has to be used to get the URL to a resource, this allows for different * alternate frontends to be used without resorting to core hacks. Note that you * do not have to use this when you are navigating between pages of your own plugin. * * To set another resolver, set the following config value in config.php: * $CFG->core_competency_url_resolver = 'your_plugin\\your_url_resolver_class'; * * Your URL resolver should implement the same methods as the ones listed in * this class (except for {{@link self::get()}}) but not statically. * * /!\ Note, resolvers MUST NEVER assume that the resource, or the resources * represented by the arguments, still exist. * * @package core_competency * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class url { /** @var url_resolver The URL resolver instance.*/ protected static $resolver; /** * Defer to the resolver. * * @param string $resource The resource type. * @param array $args The arguments. * @return mixed */ protected static function get($resource, $args) { global $CFG; if (!isset(static::$resolver)) { $klass = !empty($CFG->core_competency_url_resolver) ? $CFG->core_competency_url_resolver : 'tool_lp\\url_resolver'; static::$resolver = new $klass(); } if (!method_exists(static::$resolver, $resource)) { debugging("URL for '$resource' not implemented.", DEBUG_DEVELOPER); return new moodle_url('/'); } return call_user_func_array([static::$resolver, $resource], $args); } /** * The URL where the competency can be found. * * @param int $competencyid The competency ID. * @param int $pagecontextid The ID of the context we are in. * @return moodle_url */ public static function competency($competencyid, $pagecontextid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the framework can be found. * * @param int $frameworkid The framework ID. * @param int $pagecontextid The ID of the context we are in. * @return moodle_url */ public static function framework($frameworkid, $pagecontextid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the frameworks can be found. * * @param int $pagecontextid The ID of the context that we are browsing. * @return moodle_url */ public static function frameworks($pagecontextid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the plan can be found. * * @param int $planid The plan ID. * @return moodle_url */ public static function plan($planid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the plans of a user can be found. * * @param int $userid The user ID. * @return moodle_url */ public static function plans($userid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the template can be found. * * @param int $templateid The template ID. * @param int $pagecontextid The ID of the context we are in. * @return moodle_url */ public static function template($templateid, $pagecontextid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the templates can be found. * * @param int $pagecontextid The ID of the context that we are browsing. * @return moodle_url */ public function templates($pagecontextid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the user competency can be found. * * @param int $usercompetencyid The user competency ID * @return moodle_url */ public static function user_competency($usercompetencyid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the user competency can be found in the context of a course. * * @param int $userid The user ID * @param int $competencyid The competency ID. * @param int $courseid The course ID. * @return moodle_url */ public static function user_competency_in_course($userid, $competencyid, $courseid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the user competency can be found in the context of a plan. * * @param int $userid The user ID * @param int $competencyid The competency ID. * @param int $planid The plan ID. * @return moodle_url */ public static function user_competency_in_plan($userid, $competencyid, $planid) { return static::get(__FUNCTION__, func_get_args()); } /** * The URL where the user evidence (of prior learning) can be found. * * @param int $userevidenceid The user evidence ID * @return moodle_url */ public static function user_evidence($userevidenceid) { return static::get(__FUNCTION__, func_get_args()); } } plan_competency.php 0000644 00000012560 15151776373 0010460 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for plan_competency persistence. * * @package core_competency * @copyright 2015 Issam Taboubi <issam.taboubi@umontreal.ca> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use lang_string; /** * Class for managing competencies in the plan (add/remove competencies for given plan). * * @copyright 2015 Issam Taboubi <issam.taboubi@umontreal.ca> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plan_competency extends persistent { /** Table name for plan_competency persistency */ const TABLE = 'competency_plancomp'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'planid' => array( 'type' => PARAM_INT, ), 'competencyid' => array( 'type' => PARAM_INT, ), 'sortorder' => array( 'type' => PARAM_INT, 'default' => null, ), ); } /** * List the competencies in this plan. * * @param int $planid The plan id * @return array[competency] */ public static function list_competencies($planid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} plancomp ON plancomp.competencyid = comp.id WHERE plancomp.planid = ? ORDER BY plancomp.sortorder ASC, plancomp.id ASC'; $params = array($planid); // TODO MDL-52229 Handle hidden competencies. $results = $DB->get_records_sql($sql, $params); $instances = array(); foreach ($results as $result) { array_push($instances, new competency(0, $result)); } return $instances; } /** * Get a single competency from the plan (only if it is really in the plan). * * @param int $planid The plan id * @param int $competencyid The competency id * @return competency */ public static function get_competency($planid, $competencyid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} plncomp ON plncomp.competencyid = comp.id WHERE plncomp.planid = ? AND plncomp.competencyid = ?'; $params = array($planid, $competencyid); $result = $DB->get_record_sql($sql, $params); if (!$result) { throw new \coding_exception('The competency does not belong to this plan: ' . $competencyid . ', ' . $planid); } return new competency(0, $result); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { if (($this->get('id') && $this->get('sortorder') === null) || !$this->get('id')) { $this->set('sortorder', $this->count_records(array('planid' => $this->get('planid')))); } } /** * Validate competencyid. * * @param int $value ID. * @return true|lang_string */ protected function validate_competencyid($value) { if (!competency::record_exists($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate planid. * * @param int $value ID. * @return true|lang_string */ protected function validate_planid($value) { if (!plan::record_exists($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Hook to execute after delete. * * @param bool $result Whether or not the delete was successful. * @return void */ protected function after_delete($result) { global $DB; if (!$result) { return; } $table = '{' . self::TABLE . '}'; $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE planid = ? AND sortorder > ?"; $DB->execute($sql, array($this->get('planid'), $this->get('sortorder'))); } /** * Check if plan competency has records for competencies. * * @param array $competencyids The competences IDs * @return boolean */ public static function has_records_for_competencies($competencyids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); return self::record_exists_select("competencyid $insql", $params); } } template_competency.php 0000644 00000021527 15151776373 0011344 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing competencies from the DB. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use stdClass; /** * Class for loading/storing template_competencies from the DB. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_competency extends persistent { const TABLE = 'competency_templatecomp'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'templateid' => array( 'type' => PARAM_INT, 'default' => 0, ), 'competencyid' => array( 'type' => PARAM_INT, 'default' => 0, ), 'sortorder' => array( 'type' => PARAM_INT, 'default' => 0, ), ); } /** * Count the templates using a competency. * * @param int $competencyid The competency id * @param bool $onlyvisible If true, only count visible templates using this competency. * @return int */ public static function count_templates($competencyid, $onlyvisible) { global $DB; $sql = 'SELECT COUNT(tpl.id) FROM {' . self::TABLE . '} tplcomp JOIN {' . template::TABLE . '} tpl ON tplcomp.templateid = tpl.id WHERE tplcomp.competencyid = ? '; $params = array($competencyid); if ($onlyvisible) { $sql .= ' AND tpl.visible = ?'; $params[] = 1; } $results = $DB->count_records_sql($sql, $params); return $results; } /** * List the templates using a competency. * * @param int $competencyid The competency id * @param bool $onlyvisible If true, only count visible templates using this competency. * @return array[competency] */ public static function list_templates($competencyid, $onlyvisible) { global $DB; $sql = 'SELECT tpl.* FROM {' . template::TABLE . '} tpl JOIN {' . self::TABLE . '} tplcomp ON tplcomp.templateid = tpl.id WHERE tplcomp.competencyid = ? '; $params = array($competencyid); if ($onlyvisible) { $sql .= ' AND tpl.visible = ?'; $params[] = 1; } $sql .= ' ORDER BY tpl.id ASC'; $results = $DB->get_records_sql($sql, $params); $instances = array(); foreach ($results as $result) { array_push($instances, new template(0, $result)); } return $instances; } /** * Count the competencies in a template. * * @param int $templateid The template id * @return int */ public static function count_competencies($templateid) { global $DB; $sql = 'SELECT COUNT(comp.id) FROM {' . self::TABLE . '} tplcomp JOIN {' . competency::TABLE . '} comp ON tplcomp.competencyid = comp.id WHERE tplcomp.templateid = ? '; $params = array($templateid); $results = $DB->count_records_sql($sql, $params); return $results; } /** * Count the competencies in a template with no links to courses. * * @param int $templateid The template id * @return int */ public static function count_competencies_with_no_courses($templateid) { global $DB; $sql = 'SELECT COUNT(comp.id) FROM {' . self::TABLE . '} tplcomp JOIN {' . competency::TABLE . '} comp ON tplcomp.competencyid = comp.id LEFT JOIN {' . course_competency::TABLE . '} crscomp ON crscomp.competencyid = comp.id WHERE tplcomp.templateid = ? AND crscomp.id IS NULL'; $params = array($templateid); $results = $DB->count_records_sql($sql, $params); return $results; } /** * Get a single competency from the template (only if it is really in the template). * * @param int $templateid The template id * @param int $competencyid The competency id * @return competency */ public static function get_competency($templateid, $competencyid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} tplcomp ON tplcomp.competencyid = comp.id WHERE tplcomp.templateid = ? AND tplcomp.competencyid = ?'; $params = array($templateid, $competencyid); $result = $DB->get_record_sql($sql, $params); if (!$result) { throw new \coding_exception('The competency does not belong to this template: ' . $competencyid . ', ' . $templateid); } return new competency(0, $result); } /** * List the competencies in this template. * * @param int $templateid The template id * @return array[competency] */ public static function list_competencies($templateid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} tplcomp ON tplcomp.competencyid = comp.id WHERE tplcomp.templateid = ? ORDER BY tplcomp.sortorder ASC, tplcomp.id ASC'; $params = array($templateid); $results = $DB->get_records_sql($sql, $params); $instances = array(); foreach ($results as $result) { array_push($instances, new competency(0, $result)); } return $instances; } /** * Remove the competencies in this template. * * @param int $templateid The template id * @return boolen */ public static function delete_by_templateid($templateid) { global $DB; return $DB->delete_records(self::TABLE, array('templateid' => $templateid)); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { if (($this->get('id') && $this->get('sortorder') === null) || !$this->get('id')) { $this->set('sortorder', $this->count_records(array('templateid' => $this->get('templateid')))); } } /** * Validate competencyid. * * @param int $value ID. * @return true|lang_string */ protected function validate_competencyid($value) { if (!competency::record_exists($value)) { return new \lang_string('invaliddata', 'error'); } return true; } /** * Validate templateid. * * @param int $value ID. * @return true|lang_string */ protected function validate_templateid($value) { if (!template::record_exists($value)) { return new \lang_string('invaliddata', 'error'); } return true; } /** * Hook to execute after delete. * * @param bool $result Whether or not the delete was successful. * @return void */ protected function after_delete($result) { global $DB; if (!$result) { return; } $table = '{' . self::TABLE . '}'; $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE templateid = ? AND sortorder > ?"; $DB->execute($sql, array($this->get('templateid'), $this->get('sortorder'))); } /** * Check if template competency has records for competencies. * * @param array $competencyids Array of competencies ids. * @return boolean Return true if competencies were found in template_competency. */ public static function has_records_for_competencies($competencyids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); return self::record_exists_select("competencyid $insql", $params); } } competency_rule_points.php 0000644 00000015231 15151776373 0012067 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Competency rule points based. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use coding_exception; use lang_string; /** * Competency rule points based class. * * This rule matches when related competencies contribute for a required number of points. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class competency_rule_points extends competency_rule { /** * Get the rule config. * * @return mixed */ protected function get_config() { $config = parent::get_config(); return json_decode($config); } /** * Whether or not the rule is matched. * * @param user_competency $usercompetency The user competency. * @return bool */ public function matches(user_competency $usercompetency) { global $DB; $config = $this->get_config(); $pointsrequired = $config->base->points; // Index by competency ID and extract required. $compsrules = array(); $requiredids = array(); foreach ($config->competencies as $comp) { $compsrules[$comp->id] = $comp; if ($comp->required) { $requiredids[$comp->id] = $comp->id; } } // Find all the user competency records. list($insql, $params) = $DB->get_in_or_equal(array_keys($compsrules), SQL_PARAMS_NAMED); $sql = "userid = :userid AND proficiency = :proficiency AND competencyid $insql"; $params['userid'] = $usercompetency->get('userid'); $params['proficiency'] = 1; $ucs = user_competency::get_records_select($sql, $params, '', 'competencyid'); // Check that all the required are found. if (!empty($requiredids)) { $unmetrequired = array_diff_key($requiredids, $ucs); if (!empty($unmetrequired)) { return false; } } // Check that we have enough points. $points = 0; foreach ($compsrules as $compid => $comp) { if (array_key_exists($compid, $ucs)) { $points += $comp->points; } } return $points >= $pointsrequired; } /** * Validate the rule config. * * @param string $value The value to validate. * @return bool */ public function validate_config($value) { $compids = array(); $config = json_decode($value); if ($config === null || !isset($config->base) || !isset($config->competencies)) { return false; } if (!isset($config->base->points)) { return false; } try { $requiredpoints = validate_param($config->base->points, PARAM_INT); } catch (\invalid_parameter_exception $e) { return false; } if ($requiredpoints < 1) { return false; } $totalpoints = 0; // Validate the competency info. foreach ($config->competencies as $competency) { // Cannot include self. if ($competency->id == $this->competency->get('id')) { return false; } // Check for duplicates. if (in_array($competency->id, $compids)) { return false; } // Check for required fields. if (!isset($competency->id) || !isset($competency->points) || !isset($competency->required)) { return false; } // Validate the parameters. try { validate_param($competency->id, PARAM_INT); $points = validate_param($competency->points, PARAM_INT); validate_param($competency->required, PARAM_BOOL); } catch (\invalid_parameter_exception $e) { return false; } $totalpoints += $points; if ($points < 0) { return false; } $compids[] = $competency->id; } // No competencies, that's strange. if (empty($compids)) { return false; } // Impossible to reach the points required. if ($requiredpoints > $totalpoints) { return false; } // Check that all the competencies are children of the competency. // We may want to relax this check at a later stage if we want to allow competencies // to be linked throughout the whole framework. return $this->competency->is_parent_of($compids); } /** * The name of the rule. * * @return lang_string */ public static function get_name() { return new lang_string('pointsrequiredaremet', 'core_competency'); } /** * Migrate rule config when duplicate competency based on mapping competencies ids. * * @param string $config the config rule of a competency * @param array $mappings array that match the old competency ids with the new competencies * @return string */ public static function migrate_config($config, $mappings) { $ruleconfig = json_decode($config, true); if (is_array($ruleconfig)) { foreach ($ruleconfig['competencies'] as $key => $rulecomp) { $rulecmpid = $rulecomp['id']; if (array_key_exists($rulecmpid, $mappings)) { $ruleconfig['competencies'][$key]['id'] = $mappings[$rulecmpid]->get('id'); } else { throw new coding_exception("The competency id is not found in the matchids."); } } } else { throw new coding_exception("Invalid JSON config rule."); } return json_encode($ruleconfig); } } course_competency.php 0000644 00000033413 15151776373 0011026 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing competencies from the DB. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; use coding_exception; use lang_string; use core_course\external\course_summary_exporter; /** * Class for loading/storing course_competencies from the DB. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_competency extends persistent { const TABLE = 'competency_coursecomp'; /** Course competency ruleoutcome constant. */ const OUTCOME_NONE = 0; /** Course competency ruleoutcome constant. */ const OUTCOME_EVIDENCE = 1; /** Course competency ruleoutcome constant. */ const OUTCOME_RECOMMEND = 2; /** Course competency ruleoutcome constant. */ const OUTCOME_COMPLETE = 3; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'courseid' => array( 'type' => PARAM_INT ), 'competencyid' => array( 'type' => PARAM_INT ), 'sortorder' => array( 'type' => PARAM_INT ), 'ruleoutcome' => array( 'choices' => array(self::OUTCOME_NONE, self::OUTCOME_EVIDENCE, self::OUTCOME_RECOMMEND, self::OUTCOME_COMPLETE ), 'default' => self::OUTCOME_EVIDENCE, 'type' => PARAM_INT, ), ); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { if (($this->get('id') && $this->get('sortorder') === null) || !$this->get('id')) { $this->set('sortorder', $this->count_records(array('courseid' => $this->get('courseid')))); } } /** * Return the courses where both competency and user are. * * A user is considered being in a course when they are enrolled, the enrolment is valid, * the enrolment instance is enabled, and the enrolment plugin is enabled.. * * @param int $competencyid The competency ID. * @param int $userid The user ID. * @return array Indexed by course ID. */ public static function get_courses_with_competency_and_user($competencyid, $userid) { global $CFG, $DB; if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) { return array(); } $ctxfields = \context_helper::get_preload_record_columns_sql('ctx'); list($plugins, $params) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee'); $params['competencyid'] = $competencyid; $params['userid'] = $userid; $params['enabled'] = ENROL_INSTANCE_ENABLED; $params['active'] = ENROL_USER_ACTIVE; $params['contextlevel'] = CONTEXT_COURSE; // Heavily based on enrol_get_shared_courses(). $sql = "SELECT c.*, $ctxfields FROM {course} c JOIN {" . static::TABLE . "} cc ON cc.courseid = c.id AND cc.competencyid = :competencyid JOIN ( SELECT DISTINCT c.id FROM {enrol} e JOIN {user_enrolments} ue ON ue.enrolid = e.id AND ue.status = :active AND ue.userid = :userid JOIN {course} c ON c.id = e.courseid WHERE e.status = :enabled AND e.enrol $plugins ) ec ON ec.id = c.id LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel ORDER BY c.id"; $courses = $DB->get_records_sql($sql, $params); array_map('context_helper::preload_from_record', $courses); return $courses; } /** * Return a list of rules. * * @return array Indexed by outcome value. */ public static function get_ruleoutcome_list() { static $list = null; if ($list === null) { $list = array( self::OUTCOME_NONE => self::get_ruleoutcome_name(self::OUTCOME_NONE), self::OUTCOME_EVIDENCE => self::get_ruleoutcome_name(self::OUTCOME_EVIDENCE), self::OUTCOME_RECOMMEND => self::get_ruleoutcome_name(self::OUTCOME_RECOMMEND), self::OUTCOME_COMPLETE => self::get_ruleoutcome_name(self::OUTCOME_COMPLETE)); } return $list; } /** * Human readable rule name. * * @param int $ruleoutcome The value of ruleoutcome. * @return lang_string */ public static function get_ruleoutcome_name($ruleoutcome) { switch ($ruleoutcome) { case self::OUTCOME_NONE: $strname = 'none'; break; case self::OUTCOME_EVIDENCE: $strname = 'evidence'; break; case self::OUTCOME_RECOMMEND: $strname = 'recommend'; break; case self::OUTCOME_COMPLETE: $strname = 'complete'; break; default: throw new \moodle_exception('errorcoursecompetencyrule', 'core_competency', '', $ruleoutcome); break; } return new lang_string('coursecompetencyoutcome_' . $strname, 'core_competency'); } /** * Validate course ID. * * @param int $data The course ID. * @return true|lang_string */ protected function validate_courseid($data) { global $DB; if (!$DB->record_exists('course', array('id' => $data))) { return new lang_string('invalidcourseid', 'error'); } return true; } /** * Validate competency ID. * * @param int $data The competency ID. * @return true|lang_string */ protected function validate_competencyid($data) { if (!competency::record_exists($data)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Return the course IDs and visible flags that include this competency. * * Only the ids and visible flag are returned, for the full records use list_courses. * * @param int $competencyid The competency id * @return array containing courseid and visible. */ public static function list_courses_min($competencyid) { global $DB; $results = $DB->get_records_sql('SELECT course.id as id, course.visible as visible FROM {' . self::TABLE . '} coursecomp JOIN {course} course ON coursecomp.courseid = course.id WHERE coursecomp.competencyid = ? ', array($competencyid)); return $results; } /** * Return partial course records foreach course that contains this competency. * * @param int $competencyid The competency id * @return array[stdClass] Array of course records containg id, visible, shortname, idnumber, fullname */ public static function list_courses($competencyid) { global $DB; // We need all the course summary exporter properties, plus category. $coursefields = course_summary_exporter::properties_definition(); $coursefields = array_map(function(string $field): string { return "course.{$field}"; }, array_keys($coursefields)); $results = $DB->get_records_sql('SELECT ' . implode(',', $coursefields) . ', course.category FROM {course} course JOIN {' . self::TABLE . '} coursecomp ON coursecomp.courseid = course.id WHERE coursecomp.competencyid = ? ', array($competencyid)); return $results; } /** * Count the competencies in this course. * * @param int $courseid The course id * @return int */ public static function count_competencies($courseid) { global $DB; $sql = 'SELECT COUNT(comp.id) FROM {' . self::TABLE . '} coursecomp JOIN {' . competency::TABLE . '} comp ON coursecomp.competencyid = comp.id WHERE coursecomp.courseid = ? '; $params = array($courseid); $results = $DB->count_records_sql($sql, $params); return $results; } /** * List the competencies in this course. * * @param int $courseid The course id * @return competency[] Indexed by competency ID. */ public static function list_competencies($courseid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} coursecomp ON coursecomp.competencyid = comp.id WHERE coursecomp.courseid = ?'; $params = array($courseid); $sql .= ' ORDER BY coursecomp.sortorder ASC'; $results = $DB->get_recordset_sql($sql, $params); $instances = array(); foreach ($results as $result) { $comp = new competency(0, $result); $instances[$comp->get('id')] = $comp; } $results->close(); return $instances; } /** * Get a single competency from the course (only if it is really in the course). * * @param int $courseid The course id * @param int $competencyid The competency id * @return competency */ public static function get_competency($courseid, $competencyid) { global $DB; $sql = 'SELECT comp.* FROM {' . competency::TABLE . '} comp JOIN {' . self::TABLE . '} crscomp ON crscomp.competencyid = comp.id WHERE crscomp.courseid = ? AND crscomp.competencyid = ?'; $params = array($courseid, $competencyid); $result = $DB->get_record_sql($sql, $params); if (!$result) { throw new coding_exception('The competency does not belong to this course: ' . $competencyid . ', ' . $courseid); } return new competency(0, $result); } /** * Hook to execute after delete. * * @param bool $result Whether or not the delete was successful. * @return void */ protected function after_delete($result) { global $DB; if (!$result) { return; } $table = '{' . self::TABLE . '}'; $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE courseid = ? AND sortorder > ?"; $DB->execute($sql, array($this->get('courseid'), $this->get('sortorder'))); } /** * Get the specified course_competency in this course. * * @param int $courseid The course id * @param int $competencyid The competency id * @return course_competency */ public static function get_course_competency($courseid, $competencyid) { global $DB; $sql = 'SELECT crscomp.* FROM {' . self::TABLE . '} crscomp WHERE crscomp.courseid = ? AND crscomp.competencyid = ?'; $params = array($courseid, $competencyid); $result = $DB->get_record_sql($sql, $params); if (!$result) { throw new coding_exception('The competency does not belong to this course: ' . $competencyid . ', ' . $courseid); } return new course_competency(0, $result); } /** * List the course_competencies in this course. * * @param int $courseid The course id * @return course_competency[] */ public static function list_course_competencies($courseid) { global $DB; $sql = 'SELECT coursecomp.* FROM {' . self::TABLE . '} coursecomp JOIN {' . competency::TABLE . '} comp ON coursecomp.competencyid = comp.id WHERE coursecomp.courseid = ?'; $params = array($courseid); $sql .= ' ORDER BY coursecomp.sortorder ASC'; $results = $DB->get_recordset_sql($sql, $params); $instances = array(); foreach ($results as $result) { array_push($instances, new course_competency(0, $result)); } $results->close(); return $instances; } /** * Check if course competency has records for competencies. * * @param array $competencyids Array of competencies ids. * @return boolean Return true if one or more than a competency was found in a course. */ public static function has_records_for_competencies($competencyids) { global $DB; list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); return self::record_exists_select("competencyid $insql", $params); } } user_evidence.php 0000644 00000013237 15151776373 0010122 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * User evidence persistent. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use context_user; use lang_string; /** * User evidence persistent class. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_evidence extends persistent { const TABLE = 'competency_userevidence'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'userid' => array( 'type' => PARAM_INT ), 'name' => array( 'type' => PARAM_TEXT ), 'description' => array( 'type' => PARAM_CLEANHTML, 'default' => '', ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML, ), 'url' => array( 'type' => PARAM_URL, 'default' => '', 'message' => new lang_string('invalidurl', 'core_competency') ) ); } /** * Can the current user manage this user evidence? * * @return bool */ public function can_manage() { return self::can_manage_user($this->get('userid')); } /** * Can the current user view this user evidence? * * @return bool */ public function can_read() { return self::can_read_user($this->get('userid')); } /** * Get the context of this user evidence. * * @return context */ public function get_context() { return context_user::instance($this->get('userid')); } /** * Get link competencies. */ public function get_competencies() { return user_evidence_competency::get_competencies_by_userevidenceid($this->get('id')); } /** * Get link user competencies. */ public function get_user_competencies() { return user_evidence_competency::get_user_competencies_by_userevidenceid($this->get('id')); } /** * Return true if the user of the evidence has plan. * * @return bool */ public function user_has_plan() { return plan::record_exists_select('userid = ?', array($this->get('userid'))); } /** * Return the files associated with this evidence. * * @return object[] */ public function get_files() { $fs = get_file_storage(); $files = $fs->get_area_files($this->get_context()->id, 'core_competency', 'userevidence', $this->get('id'), 'filename', false); return $files; } /** * Validate the URL. * * @param int $value * @return true|lang_string */ protected function validate_url($value) { if (empty($value) && !is_numeric($value)) { return true; } if (!preg_match('@^https?://.+@', $value)) { return new lang_string('invalidurl', 'core_competency'); } return true; } /** * Validate the user ID. * * @param int $value * @return true|lang_string */ protected function validate_userid($value) { global $DB; // During create. if (!$this->get('id')) { // Check that the user exists. We do not need to do that on update because // the userid of an evidence should never change. if (!$DB->record_exists('user', array('id' => $value))) { return new lang_string('invaliddata', 'error'); } } return true; } /** * Can the current user manage a user's evidence? * * @param int $evidenceuserid The user to whom the evidence would belong. * @return bool */ public static function can_manage_user($evidenceuserid) { global $USER; $context = context_user::instance($evidenceuserid); $capabilities = array('moodle/competency:userevidencemanage'); if ($context->instanceid == $USER->id) { $capabilities[] = 'moodle/competency:userevidencemanageown'; } return has_any_capability($capabilities, $context); } /** * Can the current user view a user's evidence? * * @param int $evidenceuserid The user to whom the evidence would belong. * @return bool */ public static function can_read_user($evidenceuserid) { $context = context_user::instance($evidenceuserid); $capabilities = array('moodle/competency:userevidenceview'); return has_any_capability($capabilities, $context) || self::can_manage_user($evidenceuserid); } } invalid_persistent_exception.php 0000644 00000002621 15151776373 0013261 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Invalid persistent exception. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); debugging('The class core_competency\\invalid_persistent_exception is deprecated. ' . 'Please use core\\invalid_persistent_exception instead.'); /** * Invalid persistent exception class. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 3.3 */ class invalid_persistent_exception extends \core\invalid_persistent_exception { } course_competency_settings.php 0000644 00000007657 15151776373 0012761 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for course_competency_settings persistence. * * @package core_competency * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; use lang_string; use context_course; defined('MOODLE_INTERNAL') || die(); /** * Class for course_competency_settings persistence. * * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_competency_settings extends persistent { /** Table name for plan_competency persistency */ const TABLE = 'competency_coursecompsetting'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'courseid' => array( 'type' => PARAM_INT, ), 'pushratingstouserplans' => array( 'type' => PARAM_BOOL, 'default' => function() { return get_config('core_competency', 'pushcourseratingstouserplans'); } ), ); } /** * Get a the course settings for a single course. * * @param int $courseid The course id * @return course_competency_settings */ public static function get_by_courseid($courseid) { global $DB; $params = array( 'courseid' => $courseid ); $settings = new static(null, (object) $params); if ($record = $DB->get_record(self::TABLE, $params)) { $settings->from_record($record); } return $settings; } /** * Can the current user view competency settings for this course. * * @param int $courseid The course ID. * @return bool */ public static function can_read($courseid) { $context = context_course::instance($courseid); $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); return has_any_capability($capabilities, $context); } /** * Can the current user change competency settings for this course. * * @param int $courseid The course ID. * @return bool */ public static function can_manage_course($courseid) { $context = context_course::instance($courseid); $capabilities = array('moodle/competency:coursecompetencyconfigure'); return has_any_capability($capabilities, $context); } /** * Can the current user change competency settings for this course. * * @return bool */ public function can_manage() { return static::can_manage_course($this->get('courseid')); } /** * Validate course ID. * * @param int $data The course ID. * @return true|lang_string */ protected function validate_courseid($data) { global $DB; if (!$DB->record_exists('course', array('id' => $data))) { return new lang_string('invalidcourseid', 'error'); } return true; } /** * Get the context. * * @return context The context */ public function get_context() { return context_course::instance($this->get('courseid')); } } user_evidence_competency.php 0000644 00000012612 15151776373 0012344 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * User evidence competency persistent. * * This represent the many to many relationship between evidence of prior * learning and competencies. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use stdClass; use lang_string; /** * User evidence competency persistent class. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_evidence_competency extends persistent { const TABLE = 'competency_userevidencecomp'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'userevidenceid' => array( 'type' => PARAM_INT ), 'competencyid' => array( 'type' => PARAM_INT, ), ); } /** * Validate competency ID. * * @param int $value ID. * @return true|lang_string */ protected function validate_competencyid($value) { if (!competency::record_exists($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Validate user evidence ID. * * @param int $value ID. * @return true|lang_string */ protected function validate_userevidenceid($value) { if (!user_evidence::record_exists($value)) { return new lang_string('invaliddata', 'error'); } return true; } /** * Get competencies by user evidence ID. * * @param int $userevidenceid The user evidence ID. * @return competency[] */ public static function get_competencies_by_userevidenceid($userevidenceid) { global $DB; $sql = "SELECT c.* FROM {" . self::TABLE . "} uec JOIN {" . competency::TABLE . "} c ON uec.userevidenceid = ? AND uec.competencyid = c.id ORDER BY c.shortname"; $competencies = array(); $records = $DB->get_recordset_sql($sql, array($userevidenceid)); foreach ($records as $record) { $competencies[] = new competency(0, $record); } $records->close(); return $competencies; } /** * Get user competencies by user evidence ID. * * @param int $userevidenceid The user evidence ID. * @return user_competency[] */ public static function get_user_competencies_by_userevidenceid($userevidenceid) { global $DB; $sql = "SELECT uc.* FROM {" . user_competency::TABLE . "} uc JOIN {" . self::TABLE . "} uec ON uc.competencyid = uec.competencyid JOIN {" . user_evidence::TABLE . "} ue ON uec.userevidenceid = ue.id AND uc.userid = ue.userid AND ue.id = ? ORDER BY uc.id ASC"; $usercompetencies = array(); $records = $DB->get_recordset_sql($sql, array($userevidenceid)); foreach ($records as $record) { $usercompetencies[] = new user_competency(0, $record); } $records->close(); return $usercompetencies; } /** * Get a relation. * * This does not perform any validation on the data passed. If the relation exists in the database * then it is loaded in a the model, if not then it is up to the developer to save the model. * * @param int $userevidenceid * @param int $competencyid * @return template_cohort */ public static function get_relation($userevidenceid, $competencyid) { global $DB; $params = array( 'userevidenceid' => $userevidenceid, 'competencyid' => $competencyid ); $relation = new static(null, (object) $params); if ($record = $DB->get_record(static::TABLE, $params)) { $relation->from_record($record); } return $relation; } /** * Delete evidences using competencies. * * @param array $competencyids Array of competencies ids. * @return bool Return true if the delete was successful. */ public static function delete_by_competencyids($competencyids) { global $DB; if (empty($competencyids)) { return true; } list($insql, $params) = $DB->get_in_or_equal($competencyids); return $DB->delete_records_select(self::TABLE, "competencyid $insql", $params); } } external/template_exporter.php 0000644 00000005353 15151776373 0012667 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting template data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use moodle_url; use renderer_base; use core_competency\plan; use core_competency\template_cohort; /** * Class for exporting template data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\template::class; } protected function get_other_values(renderer_base $output) { $context = $this->persistent->get_context(); return array( 'duedateformatted' => userdate($this->persistent->get('duedate')), 'cohortscount' => template_cohort::count_records(array('templateid' => $this->persistent->get('id'))), 'planscount' => plan::count_records(array('templateid' => $this->persistent->get('id'))), 'canmanage' => $this->persistent->can_manage(), 'canread' => $this->persistent->can_read(), 'contextname' => $context->get_context_name(), 'contextnamenoprefix' => $context->get_context_name(false) ); } protected static function define_other_properties() { return array( 'duedateformatted' => array( 'type' => PARAM_RAW ), 'cohortscount' => array( 'type' => PARAM_INT ), 'planscount' => array( 'type' => PARAM_INT ), 'canmanage' => array( 'type' => PARAM_BOOL ), 'canread' => array( 'type' => PARAM_BOOL ), 'contextname' => array( 'type' => PARAM_TEXT, ), 'contextnamenoprefix' => array( 'type' => PARAM_TEXT, ) ); } } external/competency_framework_exporter.php 0000644 00000006027 15151776373 0015276 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting competency_framework data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use core_competency\api; use renderer_base; /** * Class for exporting competency_framework data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class competency_framework_exporter extends \core\external\persistent_exporter { /** * Define the name of persistent class. * * @return string */ protected static function define_class() { return \core_competency\competency_framework::class; } /** * Get other values that do not belong to the basic persisent. * * @param renderer_base $output * @return Array */ protected function get_other_values(renderer_base $output) { $filters = array('competencyframeworkid' => $this->persistent->get('id')); $context = $this->persistent->get_context(); $competenciescount = 0; try { $competenciescount = api::count_competencies($filters); } catch (\required_capability_exception $re) { $competenciescount = 0; } return array( 'canmanage' => has_capability('moodle/competency:competencymanage', $context), 'competenciescount' => $competenciescount, 'contextname' => $context->get_context_name(), 'contextnamenoprefix' => $context->get_context_name(false) ); } /** * Define other properties that do not belong to the basic persisent. * * @return Array */ protected static function define_other_properties() { return array( 'canmanage' => array( 'type' => PARAM_BOOL ), 'competenciescount' => array( 'type' => PARAM_INT ), // Both contexts need to be PARAM_RAW because the method context::get_context_name() // already applies the formatting and thus could return HTML content. 'contextname' => array( 'type' => PARAM_RAW ), 'contextnamenoprefix' => array( 'type' => PARAM_RAW ) ); } } external/course_module_competency_exporter.php 0000644 00000002475 15151776373 0016151 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting course module competency data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * Class for exporting course module competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\course_module_competency::class; } } external/evidence_exporter.php 0000644 00000011355 15151776373 0012635 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting evidence data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use context_system; use renderer_base; use core_competency\evidence; use core_competency\user_competency; use core_user\external\user_summary_exporter; /** * Class for exporting evidence data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class evidence_exporter extends \core\external\persistent_exporter { /** * Constructor. * * @param mixed $data The data. * @param array $related Array of relateds. */ public function __construct($data, $related = array()) { if (!isset($related['context'])) { // Previous code was automatically using the system context which was not correct. // We let developers know that they must fix their code without breaking anything, and // fallback on the previous behaviour. This should be removed at a later stage: Moodle 3.5. debugging('Missing related context in evidence_exporter.', DEBUG_DEVELOPER); $related['context'] = context_system::instance(); } parent::__construct($data, $related); } protected static function define_related() { return array( 'actionuser' => 'stdClass?', 'context' => 'context', 'scale' => 'grade_scale', 'usercompetency' => 'core_competency\\user_competency?', 'usercompetencyplan' => 'core_competency\\user_competency_plan?', ); } protected static function define_class() { return evidence::class; } protected function get_other_values(renderer_base $output) { $other = array(); if (!empty($this->related['actionuser'])) { $exporter = new user_summary_exporter($this->related['actionuser']); $actionuser = $exporter->export($output); $other['actionuser'] = $actionuser; } $other['description'] = $this->persistent->get_description(); $other['userdate'] = userdate($this->persistent->get('timecreated')); if ($this->persistent->get('grade') === null) { $gradename = '-'; } else { $gradename = $this->related['scale']->scale_items[$this->persistent->get('grade') - 1]; } $other['gradename'] = $gradename; // Try to guess the user from the user competency. $userid = null; if ($this->related['usercompetency']) { $userid = $this->related['usercompetency']->get('userid'); } else if ($this->related['usercompetencyplan']) { $userid = $this->related['usercompetencyplan']->get('userid'); } else { $uc = user_competency::get_record(['id' => $this->persistent->get('usercompetencyid')]); $userid = $uc->get('userid'); } $other['candelete'] = evidence::can_delete_user($userid); return $other; } /** * Get the format parameters for gradename. * * @return array */ protected function get_format_parameters_for_gradename() { return [ 'context' => context_system::instance(), // The system context is cached, so we can get it right away. ]; } public static function define_other_properties() { return array( 'actionuser' => array( 'type' => user_summary_exporter::read_properties_definition(), 'optional' => true ), 'description' => array( 'type' => PARAM_TEXT, // The description may contain course names, etc.. which may need filtering. ), 'gradename' => array( 'type' => PARAM_TEXT, ), 'userdate' => array( 'type' => PARAM_NOTAGS ), 'candelete' => array( 'type' => PARAM_BOOL ) ); } } external/plan_competency_exporter.php 0000644 00000002431 15151776374 0014227 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting plan competency data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * Class for exporting plan competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plan_competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\plan_competency::class; } } external/user_evidence_competency_exporter.php 0000644 00000002552 15151776374 0016121 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * User evidence competency exporter. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * User evidence competency exporter class. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_evidence_competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\user_evidence_competency::class; } } external/user_evidence_exporter.php 0000644 00000007734 15151776374 0013702 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting user_evidence data. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use moodle_url; use renderer_base; use core_competency\external\performance_helper; use core_files\external\stored_file_exporter; /** * Class for exporting user_evidence data. * * @package core_competency * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_evidence_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\user_evidence::class; } protected static function define_other_properties() { return array( 'canmanage' => array( 'type' => PARAM_BOOL ), 'competencycount' => array( 'type' => PARAM_INT ), 'competencies' => array( 'type' => competency_exporter::read_properties_definition(), 'multiple' => true ), 'filecount' => array( 'type' => PARAM_INT ), 'files' => array( 'type' => stored_file_exporter::read_properties_definition(), 'multiple' => true ), 'hasurlorfiles' => array( 'type' => PARAM_BOOL ), 'urlshort' => array( 'type' => PARAM_TEXT ), ); } protected static function define_related() { return array( 'context' => 'context', 'competencies' => 'core_competency\\competency[]' ); } protected function get_other_values(renderer_base $output) { $helper = new performance_helper(); $competencies = array(); foreach ($this->related['competencies'] as $competency) { $context = $helper->get_context_from_competency($competency); $compexporter = new competency_exporter($competency, array('context' => $context)); $competencies[] = $compexporter->export($output); } $urlshort = ''; $url = $this->persistent->get('url'); if (!empty($url)) { $murl = new moodle_url($url); $shorturl = preg_replace('@^https?://(www\.)?@', '', $murl->out(false)); $urlshort = shorten_text($shorturl, 30, true); } $files = array(); $storedfiles = $this->persistent->get_files(); if (!empty($storedfiles)) { foreach ($storedfiles as $storedfile) { $fileexporter = new stored_file_exporter($storedfile, array('context' => $this->related['context'])); $files[] = $fileexporter->export($output); } } $values = array( 'canmanage' => $this->persistent->can_manage(), 'competencycount' => count($competencies), 'competencies' => $competencies, 'filecount' => count($files), 'files' => $files, 'hasurlorfiles' => !empty($files) || !empty($url), 'urlshort' => $urlshort ); return $values; } } external/related_competency_exporter.php 0000644 00000002427 15151776374 0014722 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting plan data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * Class for exporting related competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class related_competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\related_competency::class; } } external/user_competency_plan_exporter.php 0000644 00000005421 15151776374 0015267 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting plan competency data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use context_system; use renderer_base; use stdClass; /** * Class for exporting plan competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_competency_plan_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\user_competency_plan::class; } protected static function define_related() { // We cache the scale so it does not need to be retrieved from the framework every time. return array('scale' => 'grade_scale'); } protected function get_other_values(renderer_base $output) { $result = new stdClass(); if ($this->persistent->get('grade') === null) { $gradename = '-'; } else { $gradename = $this->related['scale']->scale_items[$this->persistent->get('grade') - 1]; } $result->gradename = $gradename; if ($this->persistent->get('proficiency') === null) { $proficiencyname = get_string('no'); } else { $proficiencyname = get_string($this->persistent->get('proficiency') ? 'yes' : 'no'); } $result->proficiencyname = $proficiencyname; return (array) $result; } /** * Get the format parameters for gradename. * * @return array */ protected function get_format_parameters_for_gradename() { return [ 'context' => context_system::instance(), // The system context is cached, so we can get it right away. ]; } protected static function define_other_properties() { return array( 'gradename' => array( 'type' => PARAM_TEXT ), 'proficiencyname' => array( 'type' => PARAM_RAW ), ); } } external/performance_helper.php 0000644 00000010373 15151776374 0012763 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Performance helper. * * @package core_competency * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use core_competency\competency; use core_competency\competency_framework; /** * Performance helper class. * * This tool keeps a local cache of certain items, which means that subsequent * calls to get the resource will not query the database. You will want to use * this when many resources could be shared and need to be queried in a loop. * * Note that some of these improvements can only be achieved by knowing the * logic deeper in other modules. For instance we know that a competency's context * is the one of its framework. This tool must be kept in sync with those APIs. * * @package core_competency * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class performance_helper { /** @var \context Cache of contexts by framework ID. */ protected $frameworkscontexts = []; /** @var competency_framework Cache of frameworks by framework ID. */ protected $frameworks = []; /** @var \grade_scale[] Cache of scales by scale ID. */ protected $scales = []; /** * Get the context of a competency. * * @param competency $competency The competency. * @return \context */ public function get_context_from_competency(competency $competency) { $frameworkid = $competency->get('competencyframeworkid'); if (!isset($this->frameworkscontexts[$frameworkid])) { $framework = $this->get_framework_from_competency($competency); $this->frameworkscontexts[$frameworkid] = $framework->get_context(); } return $this->frameworkscontexts[$frameworkid]; } /** * Get the framework of a competency. * * @param competency $competency The competency. * @return competency_framework */ public function get_framework_from_competency(competency $competency) { $frameworkid = $competency->get('competencyframeworkid'); if (!isset($this->frameworks[$frameworkid])) { $this->frameworks[$frameworkid] = $competency->get_framework(); } return $this->frameworks[$frameworkid]; } /** * Get the scale of a competency. * * /!\ Make sure that this is always kept in sync with: * - core_competency\competency::get_scale() * - core_competency\competency_framework::get_scale() * * @param competency $competency The competency. * @return \grade_scale */ public function get_scale_from_competency(competency $competency) { $scaleid = $competency->get('scaleid'); if ($scaleid !== null && !isset($this->scales[$scaleid])) { $this->scales[$scaleid] = $competency->get_scale(); } else if ($scaleid === null) { $framework = $this->get_framework_from_competency($competency); $scaleid = $framework->get('scaleid'); if (!isset($this->scales[$scaleid])) { $this->scales[$scaleid] = $framework->get_scale(); } } return $this->scales[$scaleid]; } /** * Ingest a framework to avoid additional fetching. * * @param competency_framework $framework The framework. * @return void */ public function ingest_framework(competency_framework $framework) { $id = $framework->get('id'); $this->frameworks[$id] = $framework; } } external/user_competency_exporter.php 0000644 00000013007 15151776374 0014254 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting user competency data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use context_system; use core_user; use renderer_base; use stdClass; use core_competency\url; use core_competency\user_competency; use core_user\external\user_summary_exporter; /** * Class for exporting user competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return user_competency::class; } protected static function define_related() { // We cache the scale so it does not need to be retrieved from the framework every time. return array('scale' => 'grade_scale'); } protected function get_other_values(renderer_base $output) { $result = new stdClass(); if ($this->persistent->get('grade') === null) { $gradename = '-'; } else { $gradename = $this->related['scale']->scale_items[$this->persistent->get('grade') - 1]; } $result->gradename = $gradename; if ($this->persistent->get('proficiency') === null) { $proficiencyname = get_string('no'); } else { $proficiencyname = get_string($this->persistent->get('proficiency') ? 'yes' : 'no'); } $result->proficiencyname = $proficiencyname; $statusname = '-'; if ($this->persistent->get('status') != user_competency::STATUS_IDLE) { $statusname = (string) user_competency::get_status_name($this->persistent->get('status')); } $result->statusname = $statusname; $result->canrequestreview = $this->persistent->can_request_review(); $result->canreview = $this->persistent->can_review(); $result->isstatusidle = $this->persistent->get('status') == user_competency::STATUS_IDLE; $result->isstatusinreview = $this->persistent->get('status') == user_competency::STATUS_IN_REVIEW; $result->isstatuswaitingforreview = $this->persistent->get('status') == user_competency::STATUS_WAITING_FOR_REVIEW; $result->isrequestreviewallowed = $result->canrequestreview && $result->isstatusidle; $result->iscancelreviewrequestallowed = $result->canrequestreview && $result->isstatuswaitingforreview; $result->isstartreviewallowed = $result->canreview && $result->isstatuswaitingforreview; $result->isstopreviewallowed = $result->canreview && $result->isstatusinreview; if (!empty($result->isstatusinreview)) { // TODO Make this more efficient. $userexporter = new user_summary_exporter(core_user::get_user($this->persistent->get('reviewerid'), '*', MUST_EXIST)); $result->reviewer = $userexporter->export($output); } $result->url = url::user_competency($this->persistent->get('id'))->out(false); return (array) $result; } /** * Get the format parameters for gradename. * * @return array */ protected function get_format_parameters_for_gradename() { return [ 'context' => context_system::instance(), // The system context is cached, so we can get it right away. ]; } protected static function define_other_properties() { return array( 'canrequestreview' => array( 'type' => PARAM_BOOL, ), 'canreview' => array( 'type' => PARAM_BOOL, ), 'gradename' => array( 'type' => PARAM_TEXT ), 'isrequestreviewallowed' => array( 'type' => PARAM_BOOL, ), 'iscancelreviewrequestallowed' => array( 'type' => PARAM_BOOL, ), 'isstartreviewallowed' => array( 'type' => PARAM_BOOL, ), 'isstopreviewallowed' => array( 'type' => PARAM_BOOL, ), 'isstatusidle' => array( 'type' => PARAM_BOOL, ), 'isstatusinreview' => array( 'type' => PARAM_BOOL, ), 'isstatuswaitingforreview' => array( 'type' => PARAM_BOOL, ), 'proficiencyname' => array( 'type' => PARAM_RAW ), 'reviewer' => array( 'type' => user_summary_exporter::read_properties_definition(), 'optional' => true ), 'statusname' => array( 'type' => PARAM_RAW ), 'url' => array( 'type' => PARAM_URL ), ); } } external/course_competency_settings_exporter.php 0000644 00000002545 15151776374 0016523 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting course_competency_settings data. * * @package core_competency * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * Class for exporting course_competency_settings data. * * @package core_competency * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_competency_settings_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\course_competency_settings::class; } } external/template_competency_exporter.php 0000644 00000002451 15151776374 0015112 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting template competency data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * Class for exporting template competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\template_competency::class; } } external/competency_exporter.php 0000644 00000002720 15151776374 0013216 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting competency data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * Class for exporting competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\competency::class; } protected static function define_related() { // We cache the context so it does not need to be retrieved from the framework every time. return array('context' => '\\context'); } } external/course_competency_exporter.php 0000644 00000002441 15151776374 0014576 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting course competency data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); /** * Class for exporting course competency data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_competency_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\course_competency::class; } } external/plan_exporter.php 0000644 00000015754 15151776374 0012015 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting plan data. * * @package core_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use core_user; use renderer_base; use stdClass; use moodle_url; use core_competency\url; use core_comment\external\comment_area_exporter; use core_user\external\user_summary_exporter; /** * Class for exporting plan data. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plan_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\plan::class; } protected static function define_related() { return array('template' => 'core_competency\\template?'); } protected function get_other_values(renderer_base $output) { $classname = static::define_class(); $status = $this->persistent->get('status'); $values = new stdClass(); $values->statusname = $this->persistent->get_statusname(); $values->isbasedontemplate = $this->persistent->is_based_on_template(); $values->canmanage = $this->persistent->can_manage(); $values->canrequestreview = $this->persistent->can_request_review(); $values->canreview = $this->persistent->can_review(); $values->canbeedited = $this->persistent->can_be_edited(); $values->isactive = $status == $classname::STATUS_ACTIVE; $values->isdraft = $status == $classname::STATUS_DRAFT; $values->iscompleted = $status == $classname::STATUS_COMPLETE; $values->isinreview = $status == $classname::STATUS_IN_REVIEW; $values->iswaitingforreview = $status == $classname::STATUS_WAITING_FOR_REVIEW; $values->isreopenallowed = $values->canmanage && $values->iscompleted; $values->iscompleteallowed = $values->canmanage && $values->isactive; $values->isunlinkallowed = $values->canmanage && !$values->iscompleted && $values->isbasedontemplate; $values->isrequestreviewallowed = false; $values->iscancelreviewrequestallowed = false; $values->isstartreviewallowed = false; $values->isstopreviewallowed = false; $values->isapproveallowed = false; $values->isunapproveallowed = false; if (!$values->isbasedontemplate) { $values->isrequestreviewallowed = $values->canrequestreview && $values->isdraft; $values->iscancelreviewrequestallowed = $values->canrequestreview && $values->iswaitingforreview; $values->isstartreviewallowed = $values->canreview && $values->iswaitingforreview; $values->isstopreviewallowed = $values->canreview && $values->isinreview; $values->isapproveallowed = $values->canreview && !$values->iscompleted && !$values->isactive; $values->isunapproveallowed = $values->canreview && $values->isactive; } $values->duedateformatted = userdate($this->persistent->get('duedate')); if ($this->persistent->is_based_on_template()) { $exporter = new template_exporter($this->related['template']); $values->template = $exporter->export($output); } if (!empty($values->isinreview)) { // TODO Make this more efficient. $userexporter = new user_summary_exporter(core_user::get_user($this->persistent->get('reviewerid'), '*', MUST_EXIST)); $values->reviewer = $userexporter->export($output); } $commentareaexporter = new comment_area_exporter($this->persistent->get_comment_object()); $values->commentarea = $commentareaexporter->export($output); $values->url = url::plan($this->persistent->get('id'))->out(false); return (array) $values; } public static function define_other_properties() { return array( 'statusname' => array( 'type' => PARAM_RAW, ), 'isbasedontemplate' => array( 'type' => PARAM_BOOL, ), 'canmanage' => array( 'type' => PARAM_BOOL, ), 'canrequestreview' => array( 'type' => PARAM_BOOL, ), 'canreview' => array( 'type' => PARAM_BOOL, ), 'canbeedited' => array( 'type' => PARAM_BOOL, ), 'isactive' => array( 'type' => PARAM_BOOL ), 'isdraft' => array( 'type' => PARAM_BOOL ), 'iscompleted' => array( 'type' => PARAM_BOOL ), 'isinreview' => array( 'type' => PARAM_BOOL ), 'iswaitingforreview' => array( 'type' => PARAM_BOOL ), 'isreopenallowed' => array( 'type' => PARAM_BOOL ), 'iscompleteallowed' => array( 'type' => PARAM_BOOL ), 'isunlinkallowed' => array( 'type' => PARAM_BOOL ), 'isrequestreviewallowed' => array( 'type' => PARAM_BOOL ), 'iscancelreviewrequestallowed' => array( 'type' => PARAM_BOOL ), 'isstartreviewallowed' => array( 'type' => PARAM_BOOL ), 'isstopreviewallowed' => array( 'type' => PARAM_BOOL ), 'isapproveallowed' => array( 'type' => PARAM_BOOL ), 'isunapproveallowed' => array( 'type' => PARAM_BOOL ), 'duedateformatted' => array( 'type' => PARAM_TEXT ), 'commentarea' => array( 'type' => comment_area_exporter::read_properties_definition(), ), 'reviewer' => array( 'type' => user_summary_exporter::read_properties_definition(), 'optional' => true ), 'template' => array( 'type' => template_exporter::read_properties_definition(), 'optional' => true, ), 'url' => array( 'type' => PARAM_URL ) ); } } external/user_competency_course_exporter.php 0000644 00000005442 15151776374 0015640 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting user competency course data. * * @package core_competency * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency\external; defined('MOODLE_INTERNAL') || die(); use context_system; use renderer_base; use stdClass; /** * Class for exporting user competency course data. * * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_competency_course_exporter extends \core\external\persistent_exporter { protected static function define_class() { return \core_competency\user_competency_course::class; } protected static function define_related() { // We cache the scale so it does not need to be retrieved from the framework every time. return array('scale' => 'grade_scale'); } protected function get_other_values(renderer_base $output) { $result = new stdClass(); if ($this->persistent->get('grade') === null) { $gradename = '-'; } else { $gradename = $this->related['scale']->scale_items[$this->persistent->get('grade') - 1]; } $result->gradename = $gradename; if ($this->persistent->get('proficiency') === null) { $proficiencyname = get_string('no'); } else { $proficiencyname = get_string($this->persistent->get('proficiency') ? 'yes' : 'no'); } $result->proficiencyname = $proficiencyname; return (array) $result; } /** * Get the format parameters for gradename. * * @return array */ protected function get_format_parameters_for_gradename() { return [ 'context' => context_system::instance(), // The system context is cached, so we can get it right away. ]; } protected static function define_other_properties() { return array( 'gradename' => array( 'type' => PARAM_TEXT ), 'proficiencyname' => array( 'type' => PARAM_RAW ) ); } } user_competency_course.php 0000644 00000022202 15151776374 0012057 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for user_competency_course persistence. * * @package core_competency * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_competency; defined('MOODLE_INTERNAL') || die(); use context_course; use context_user; use lang_string; /** * Class for loading/storing user_competency_course from the DB. * * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_competency_course extends persistent { /** Table name for user_competency persistency */ const TABLE = 'competency_usercompcourse'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'userid' => array( 'type' => PARAM_INT, ), 'courseid' => array( 'type' => PARAM_INT ), 'competencyid' => array( 'type' => PARAM_INT, ), 'proficiency' => array( 'type' => PARAM_BOOL, 'default' => null, 'null' => NULL_ALLOWED, ), 'grade' => array( 'type' => PARAM_INT, 'default' => null, 'null' => NULL_ALLOWED, ), ); } /** * Return the competency Object. * * @return competency Competency Object */ public function get_competency() { return new competency($this->get('competencyid')); } /** * Get the context. * * @return context The context. */ public function get_context() { return context_user::instance($this->get('userid')); } /** * Create a new user_competency_course object. * * Note, this is intended to be used to create a blank relation, for instance when * the record was not found in the database. This does not save the model. * * @param int $userid The user ID. * @param int $competencyid The competency ID. * @param int $courseid The course ID. * @return \core_competency\user_competency_course */ public static function create_relation($userid, $competencyid, $courseid) { $data = new \stdClass(); $data->userid = $userid; $data->competencyid = $competencyid; $data->courseid = $courseid; $relation = new user_competency_course(0, $data); return $relation; } /** * Validate the user ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_userid($value) { global $DB; if (!$DB->record_exists('user', array('id' => $value))) { return new lang_string('invaliduserid', 'error'); } return true; } /** * Validate the competency ID. * * @param int $value The value. * @return true|lang_string */ protected function validate_competencyid($value) { if (!competency::record_exists($value)) { return new lang_string('errornocompetency', 'core_competency', $value); } return true; } /** * Validate course ID. * * @param int $value The course ID. * @return true|lang_string */ protected function validate_courseid($value) { if (!context_course::instance($value, IGNORE_MISSING)) { return new lang_string('errorinvalidcourse', 'core_competency', $value); } return true; } /** * Validate the proficiency. * * @param int $value The value. * @return true|lang_string */ protected function validate_proficiency($value) { $grade = $this->get('grade'); if ($grade !== null && $value === null) { // We must set a proficiency when we set a grade. return new lang_string('invaliddata', 'error'); } else if ($grade === null && $value !== null) { // We must not set a proficiency when we don't set a grade. return new lang_string('invaliddata', 'error'); } return true; } /** * Validate the grade. * * @param int $value The value. * @return true|lang_string */ protected function validate_grade($value) { if ($value !== null) { if ($value <= 0) { return new lang_string('invalidgrade', 'core_competency'); } // TODO MDL-52243 Use a core method to validate the grade_scale item. // Check if grade exist in the scale item values. $competency = $this->get_competency(); if (!array_key_exists($value - 1 , $competency->get_scale()->scale_items)) { return new lang_string('invalidgrade', 'core_competency'); } } return true; } /** * Get multiple user_competency_course for a user. * * @param int $userid * @param int $courseid * @param array $competenciesorids Limit search to those competencies, or competency IDs. * @return \core_competency\user_competency_course[] */ public static function get_multiple($userid, $courseid, array $competenciesorids = null) { global $DB; $params = array(); $params['userid'] = $userid; $params['courseid'] = $courseid; $sql = '1 = 1'; if (!empty($competenciesorids)) { $test = reset($competenciesorids); if (is_number($test)) { $ids = $competenciesorids; } else { $ids = array(); foreach ($competenciesorids as $comp) { $ids[] = $comp->get('id'); } } list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); $params += $inparams; $sql = "competencyid $insql"; } // Order by ID to prevent random ordering. return self::get_records_select("userid = :userid AND courseid = :courseid AND $sql", $params, 'id ASC'); } /** * Count the proficient competencies in this course for one user. * * @param int $courseid The course id * @param int $userid The user id * @return int */ public static function count_proficient_competencies($courseid, $userid) { global $DB; $sql = 'SELECT COUNT(comp.id) FROM {' . self::TABLE . '} usercoursecomp JOIN {' . course_competency::TABLE . '} cc ON usercoursecomp.competencyid = cc.competencyid AND cc.courseid = usercoursecomp.courseid JOIN {' . competency::TABLE . '} comp ON usercoursecomp.competencyid = comp.id WHERE usercoursecomp.courseid = ? AND usercoursecomp.userid = ? AND usercoursecomp.proficiency = ?'; $params = array($courseid, $userid, true); $results = $DB->count_records_sql($sql, $params); return $results; } /** * Get the list of competencies that were completed the least times in a course. * * @param int $courseid * @param int $skip The number of competencies to skip * @param int $limit The max number of competencies to return * @return competency[] */ public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 0) { global $DB; $fields = competency::get_sql_fields('c', 'c_'); $params = array('courseid' => $courseid); $sql = 'SELECT ' . $fields . ' FROM (SELECT cc.competencyid, SUM(COALESCE(ucc.proficiency, 0)) AS timesproficient FROM {' . course_competency::TABLE . '} cc LEFT JOIN {' . self::TABLE . '} ucc ON ucc.competencyid = cc.competencyid AND ucc.courseid = cc.courseid WHERE cc.courseid = :courseid GROUP BY cc.competencyid ) p JOIN {' . competency::TABLE . '} c ON c.id = p.competencyid ORDER BY p.timesproficient ASC, c.id DESC'; $results = $DB->get_records_sql($sql, $params, $skip, $limit); $comps = array(); foreach ($results as $r) { $c = competency::extract_record($r, 'c_'); $comps[] = new competency(0, $c); } return $comps; } } util.php 0000644 00000005723 15152001201 0006224 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * PayPal enrolment plugin utility class. * * @package enrol_paypal * @copyright 2016 Cameron Ball <cameron@cameron1729.xyz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_paypal; defined('MOODLE_INTERNAL') || die(); /** * PayPal enrolment plugin utility class. * * @package enrol_paypal * @copyright 2016 Cameron Ball <cameron@cameron1729.xyz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class util { /** * Alerts site admin of potential problems. * * @param string $subject email subject * @param stdClass $data PayPal IPN data */ public static function message_paypal_error_to_admin($subject, $data) { $admin = get_admin(); $site = get_site(); $message = "$site->fullname: Transaction failed.\n\n$subject\n\n"; foreach ($data as $key => $value) { $message .= "$key => $value\n"; } $eventdata = new \core\message\message(); $eventdata->courseid = empty($data->courseid) ? SITEID : $data->courseid; $eventdata->modulename = 'moodle'; $eventdata->component = 'enrol_paypal'; $eventdata->name = 'paypal_enrolment'; $eventdata->userfrom = $admin; $eventdata->userto = $admin; $eventdata->subject = "PAYPAL ERROR: ".$subject; $eventdata->fullmessage = $message; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->smallmessage = ''; message_send($eventdata); } /** * Silent exception handler. * * @return callable exception handler */ public static function get_exception_handler() { return function($ex) { $info = get_exception_info($ex); $logerrmsg = "enrol_paypal IPN exception handler: ".$info->message; if (debugging('', DEBUG_NORMAL)) { $logerrmsg .= ' Debug: '.$info->debuginfo."\n".format_backtrace($info->backtrace, true); } error_log($logerrmsg); if (http_response_code() == 200) { http_response_code(500); } exit(0); }; } } task/process_expirations.php 0000644 00000003215 15152001201 0012306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Process expirations task. * * @package enrol_paypal * @author Farhan Karmali <farhan6318@gmail.com> * @copyright Farhan Karmali * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace enrol_paypal\task; defined('MOODLE_INTERNAL') || die(); /** * Process expirations task. * * @package enrol_paypal * @author Farhan Karmali <farhan6318@gmail.com> * @copyright Farhan Karmali * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_expirations extends \core\task\scheduled_task { /** * Name for this task. * * @return string */ public function get_name() { return get_string('processexpirationstask', 'enrol_paypal'); } /** * Run task for processing expirations. */ public function execute() { $enrol = enrol_get_plugin('paypal'); $trace = new \text_progress_trace(); $enrol->process_expirations($trace); } } output/table.php 0000644 00000016622 15152001263 0007706 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Renderable for display of license manager table. * * @package tool_licensemanager * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_licensemanager\output; use html_table; use html_table_cell; use html_table_row; use html_writer; use license_manager; defined('MOODLE_INTERNAL') || die(); /** * Renderable for display of license manager table. * * @package tool_licensemanager * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class table implements \renderable { /** * 'Create License' link. * * @return string HTML string. */ public function create_license_link() { $link = html_writer::link(\tool_licensemanager\helper::get_create_license_url(), get_string('createlicensebuttontext', 'tool_licensemanager'), ['class' => 'btn btn-secondary mb-3']); return $link; } /** * Create the HTML table for license management. * * @param array $licenses * @param \renderer_base $output * * @return string HTML for license manager table. */ public function create_license_manager_table(array $licenses, \renderer_base $output) { $table = new html_table(); $table->head = [ get_string('enable'), get_string('license', 'tool_licensemanager'), get_string('version'), get_string('order'), get_string('edit'), get_string('delete'), ]; $table->colclasses = [ 'text-center', 'text-left', 'text-left', 'text-center', 'text-center', 'text-center', ]; $table->id = 'manage-licenses'; $table->attributes['class'] = 'admintable generaltable'; $table->data = []; $rownumber = 0; $rowcount = count($licenses); foreach ($licenses as $key => $value) { $canmoveup = $rownumber > 0; $canmovedown = $rownumber < $rowcount - 1; $table->data[] = $this->get_license_table_row_data($value, $canmoveup, $canmovedown, $output); $rownumber++; } $html = html_writer::table($table); return $html; } /** * Get table row data for a license. * * @param object $license the license to populate row data for. * @param bool $canmoveup can this row move up. * @param bool $canmovedown can this row move down. * @param \renderer_base $output the renderer * * @return \html_table_row of columns values for row. */ protected function get_license_table_row_data($license, bool $canmoveup, bool $canmovedown, \renderer_base $output) { global $CFG; $summary = $license->fullname . ' ('. $license->shortname . ')'; if (!empty($license->source)) { $summary .= html_writer::empty_tag('br'); $summary .= html_writer::link($license->source, $license->source, ['target' => '_blank']); } $summarycell = new html_table_cell($summary); $summarycell->attributes['class'] = 'license-summary'; $versioncell = new html_table_cell($license->version); $versioncell->attributes['class'] = 'license-version'; $deletelicense = ''; if ($license->shortname == $CFG->sitedefaultlicense) { $hideshow = $output->pix_icon('t/locked', get_string('sitedefaultlicenselock', 'tool_licensemanager')); } else { if ($license->enabled == license_manager::LICENSE_ENABLED) { $hideshow = html_writer::link(\tool_licensemanager\helper::get_disable_license_url($license->shortname), $output->pix_icon('t/hide', get_string('disablelicensename', 'tool_licensemanager', $license->fullname))); } else { $hideshow = html_writer::link(\tool_licensemanager\helper::get_enable_license_url($license->shortname), $output->pix_icon('t/show', get_string('enablelicensename', 'tool_licensemanager', $license->fullname))); } if ($license->custom == license_manager::CUSTOM_LICENSE) { // Link url is added by the JS `delete_license` modal used for confirmation of deletion, to avoid // link being usable before JavaScript loads on page. $deletelicense = html_writer::link('#', $output->pix_icon('i/trash', get_string('deletelicensename', 'tool_licensemanager', $license->fullname)), ['class' => 'delete-license', 'data-license' => $license->shortname]); } } $hideshowcell = new html_table_cell($hideshow); $hideshowcell->attributes['class'] = 'license-status'; if ($license->custom == license_manager::CUSTOM_LICENSE) { $editlicense = html_writer::link(\tool_licensemanager\helper::get_update_license_url($license->shortname), $output->pix_icon('t/editinline', get_string('editlicensename', 'tool_licensemanager', $license->fullname)), ['class' => 'edit-license']); } else { $editlicense = ''; } $editlicensecell = new html_table_cell($editlicense); $editlicensecell->attributes['class'] = 'edit-license'; $spacer = $output->pix_icon('spacer', '', 'moodle', ['class' => 'iconsmall']); $updown = ''; if ($canmoveup) { $updown .= html_writer::link(\tool_licensemanager\helper::get_moveup_license_url($license->shortname), $output->pix_icon('t/up', get_string('movelicenseupname', 'tool_licensemanager', $license->fullname), 'moodle', ['class' => 'iconsmall']), ['class' => 'move-up']) . ''; } else { $updown .= $spacer; } if ($canmovedown) { $updown .= ' '.html_writer::link(\tool_licensemanager\helper::get_movedown_license_url($license->shortname), $output->pix_icon('t/down', get_string('movelicensedownname', 'tool_licensemanager', $license->fullname), 'moodle', ['class' => 'iconsmall']), ['class' => 'move-down']); } else { $updown .= $spacer; } $updowncell = new html_table_cell($updown); $updowncell->attributes['class'] = 'license-order'; $row = new html_table_row([$hideshowcell, $summarycell, $versioncell, $updowncell, $editlicensecell, $deletelicense]); $row->attributes['data-license'] = $license->shortname; $row->attributes['class'] = strtolower(get_string('license', 'tool_licensemanager')); return $row; } } form/edit_license.php 0000644 00000011053 15152001263 0010642 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form for creating/updating a custom license. * * @package tool_licensemanager * @copyright 2019 Tom Dickman <tom.dickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_licensemanager\form; use moodleform; use tool_licensemanager\helper; use tool_licensemanager\manager; defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); global $CFG; require_once($CFG->libdir . '/formslib.php'); /** * Form for creating/updating a custom license. * * @package tool_licensemanager * @copyright 2019 Tom Dickman <tom.dickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class edit_license extends moodleform { /** * @var string the action form is taking. */ private $action; /** * @var string license shortname if editing or empty string if creating license. */ private $licenseshortname; /** * edit_license constructor. * * @param string $action the license_manager action to be taken by form. * @param string $licenseshortname the shortname of the license to edit. */ public function __construct(string $action, string $licenseshortname) { $this->action = $action; $this->licenseshortname = $licenseshortname; if ($action == manager::ACTION_UPDATE && !empty($licenseshortname)) { parent::__construct(helper::get_update_license_url($licenseshortname)); } else { parent::__construct(helper::get_create_license_url()); } } /** * Form definition for creation and editing of licenses. */ public function definition() { $mform = $this->_form; $mform->addElement('text', 'shortname', get_string('shortname', 'tool_licensemanager')); $mform->setType('shortname', PARAM_ALPHANUMEXT); // Shortname is only editable when user is creating a license. if ($this->action != manager::ACTION_CREATE) { $mform->freeze('shortname'); } else { $mform->addRule('shortname', get_string('shortnamerequirederror', 'tool_licensemanager'), 'required'); } $mform->addElement('text', 'fullname', get_string('fullname', 'tool_licensemanager')); $mform->setType('fullname', PARAM_TEXT); $mform->addRule('fullname', get_string('fullnamerequirederror', 'tool_licensemanager'), 'required'); $mform->addElement('text', 'source', get_string('source', 'tool_licensemanager')); $mform->setType('source', PARAM_URL); $mform->addHelpButton('source', 'source', 'tool_licensemanager'); $mform->addRule('source', get_string('sourcerequirederror', 'tool_licensemanager'), 'required'); $mform->addElement('date_selector', 'version', get_string('version', 'tool_licensemanager'), get_string('from')); $mform->addHelpButton('version', 'version', 'tool_licensemanager'); $this->add_action_buttons(); } /** * Validate form data and return errors (if any). * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files) { $errors = parent::validation($data, $files); if (array_key_exists('source', $data) && !filter_var($data['source'], FILTER_VALIDATE_URL)) { $errors['source'] = get_string('invalidurl', 'tool_licensemanager'); } if (array_key_exists('version', $data) && $data['version'] > time()) { $errors['version'] = get_string('versioncannotbefuture', 'tool_licensemanager'); } return $errors; } } manager.php 0000644 00000017770 15152001263 0006676 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * License manager. * * @package tool_licensemanager * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_licensemanager; use tool_licensemanager\form\edit_license; use license_manager; use stdClass; defined('MOODLE_INTERNAL') || die(); /** * License manager, main controller for tool_licensemanager. * * @package tool_licensemanager * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** * Action for creating a new custom license. */ const ACTION_CREATE = 'create'; /** * Action for updating a custom license's details. */ const ACTION_UPDATE = 'update'; /** * Action for deleting a custom license. */ const ACTION_DELETE = 'delete'; /** * Action for disabling a custom license. */ const ACTION_DISABLE = 'disable'; /** * Action for enabling a custom license. */ const ACTION_ENABLE = 'enable'; /** * Action for displaying the license list view. */ const ACTION_VIEW_LICENSE_MANAGER = 'viewlicensemanager'; /** * Action for moving a license up order. */ const ACTION_MOVE_UP = 'moveup'; /** * Action for moving a license down order. */ const ACTION_MOVE_DOWN = 'movedown'; /** * Entry point for internal license manager. * * @param string $action the api action to carry out. * @param string|object $license the license object or shortname of license to carry action out on. */ public function execute(string $action, $license) : void { admin_externalpage_setup('licensemanager'); // Convert license to a string if it's a full license object. if (is_object($license)) { $license = $license->shortname; } $viewmanager = true; switch ($action) { case self::ACTION_DISABLE: license_manager::disable($license); break; case self::ACTION_ENABLE: license_manager::enable($license); break; case self::ACTION_DELETE: license_manager::delete($license); break; case self::ACTION_CREATE: case self::ACTION_UPDATE: $viewmanager = $this->edit($action, $license); break; case self::ACTION_MOVE_UP: case self::ACTION_MOVE_DOWN: $this->change_license_order($action, $license); break; case self::ACTION_VIEW_LICENSE_MANAGER: default: break; } if ($viewmanager) { $this->view_license_manager(); } } /** * Edit an existing license or create a new license. * * @param string $action the form action to carry out. * @param string $licenseshortname the shortname of the license to edit. * * @return bool true if license editing complete, false otherwise. */ private function edit(string $action, string $licenseshortname) : bool { if ($action != self::ACTION_CREATE && $action != self::ACTION_UPDATE) { throw new \coding_exception('license edit actions are limited to create and update'); } $form = new form\edit_license($action, $licenseshortname); if ($form->is_cancelled()) { return true; } else if ($data = $form->get_data()) { $license = new stdClass(); if ($action == self::ACTION_CREATE) { // Check that license shortname isn't already in use. if (!empty(license_manager::get_license_by_shortname($data->shortname))) { throw new \moodle_exception('duplicatelicenseshortname', 'tool_licensemanager', helper::get_licensemanager_url(), $data->shortname); } $license->shortname = $data->shortname; } else { if (empty(license_manager::get_license_by_shortname($licenseshortname))) { throw new \moodle_exception('licensenotfoundshortname', 'license', helper::get_licensemanager_url(), $licenseshortname); } $license->shortname = $licenseshortname; } $license->fullname = $data->fullname; $license->source = $data->source; // Legacy date format maintained to prevent breaking on upgrade. $license->version = date('Ymd', $data->version) . '00'; license_manager::save($license); return true; } else { $this->view_license_editor($action, $licenseshortname, $form); return false; } } /** * Change license order by moving up or down license order. * * @param string $direction which direction to move, up or down. * @param string $licenseshortname the shortname of the license to move up or down order. */ private function change_license_order(string $direction, string $licenseshortname) : void { if (!empty($licenseshortname)) { if ($direction == self::ACTION_MOVE_UP) { license_manager::change_license_sortorder(license_manager::LICENSE_MOVE_UP, $licenseshortname); } else if ($direction == self::ACTION_MOVE_DOWN) { license_manager::change_license_sortorder(license_manager::LICENSE_MOVE_DOWN, $licenseshortname); } } } /** * View the license editor to create or edit a license. * * @param string $action * @param string $licenseshortname the shortname of the license to create/edit. * @param \tool_licensemanager\form\edit_license $form the form for submitting edit data. */ private function view_license_editor(string $action, string $licenseshortname, edit_license $form) : void { global $PAGE; $renderer = $PAGE->get_renderer('tool_licensemanager'); if ($action == self::ACTION_UPDATE && $license = license_manager::get_license_by_shortname($licenseshortname)) { $return = $renderer->render_edit_licence_headers($licenseshortname); $form->set_data(['shortname' => $license->shortname]); $form->set_data(['fullname' => $license->fullname]); $form->set_data(['source' => $license->source]); $form->set_data(['version' => helper::convert_version_to_epoch($license->version)]); } else { $return = $renderer->render_create_licence_headers(); } $return .= $form->render(); $return .= $renderer->footer(); echo $return; } /** * View the license manager. */ private function view_license_manager() : void { global $PAGE; $PAGE->requires->js_call_amd('tool_licensemanager/delete_license'); $renderer = $PAGE->get_renderer('tool_licensemanager'); $html = $renderer->header(); $html .= $renderer->heading(get_string('licensemanager', 'tool_licensemanager')); $table = new \tool_licensemanager\output\table(); $html .= $renderer->render($table); $html .= $renderer->footer(); echo $html; } } output/purposes.php 0000644 00000005425 15152024213 0010476 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Purposes renderable. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\external\purpose_exporter; /** * Class containing the purposes page renderable. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purposes extends crud_element implements renderable, templatable { /** @var array $purposes All system purposes. */ protected $purposes = []; /** * Construct this renderable. * * @param \tool_dataprivacy\purpose[] $purposes */ public function __construct($purposes) { $this->purposes = $purposes; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $PAGE; $context = \context_system::instance(); $PAGE->requires->js_call_amd('tool_dataprivacy/purposesactions', 'init'); $PAGE->requires->js_call_amd('tool_dataprivacy/add_purpose', 'getInstance', [$context->id]); $data = new stdClass(); // Navigation links. $data->navigation = []; $navigationlinks = $this->get_navigation(); foreach ($navigationlinks as $navlink) { $data->navigation[] = $navlink->export_for_template($output); } $data->purposes = []; foreach ($this->purposes as $purpose) { $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]); $exportedpurpose = $exporter->export($output); $actionmenu = $this->action_menu('purpose', $exportedpurpose, $purpose); $exportedpurpose->actions = $actionmenu->export_for_template($output); $data->purposes[] = $exportedpurpose; } return $data; } } output/categories.php 0000644 00000005475 15152024213 0010750 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Categories renderable. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\external\category_exporter; /** * Class containing the categories page renderable. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class categories extends crud_element implements renderable, templatable { /** @var array $categories All system categories. */ protected $categories = []; /** * Construct this renderable. * * @param \tool_dataprivacy\category[] $categories */ public function __construct($categories) { $this->categories = $categories; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $PAGE; $context = \context_system::instance(); $PAGE->requires->js_call_amd('tool_dataprivacy/categoriesactions', 'init'); $PAGE->requires->js_call_amd('tool_dataprivacy/add_category', 'getInstance', [$context->id]); $data = new stdClass(); // Navigation links. $data->navigation = []; $navigationlinks = $this->get_navigation(); foreach ($navigationlinks as $navlink) { $data->navigation[] = $navlink->export_for_template($output); } $data->categories = []; foreach ($this->categories as $category) { $exporter = new category_exporter($category, ['context' => \context_system::instance()]); $exportedcategory = $exporter->export($output); $actionmenu = $this->action_menu('category', $exportedcategory, $category); $exportedcategory->actions = $actionmenu->export_for_template($output); $data->categories[] = $exportedcategory; } return $data; } } output/data_requests_table.php 0000644 00000037354 15152024213 0012637 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the class used for the displaying the data requests table. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); use action_menu; use action_menu_link_secondary; use coding_exception; use dml_exception; use html_writer; use moodle_url; use stdClass; use table_sql; use tool_dataprivacy\api; use tool_dataprivacy\external\data_request_exporter; defined('MOODLE_INTERNAL') || die; /** * The class for displaying the data requests table. * * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_requests_table extends table_sql { /** @var int The user ID. */ protected $userid = 0; /** @var int[] The status filters. */ protected $statuses = []; /** @var int[] The request type filters. */ protected $types = []; /** @var bool Whether this table is being rendered for managing data requests. */ protected $manage = false; /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */ protected $datarequests = []; /** @var \stdClass[] List of userids and whether they have any ongoing active requests. */ protected $ongoingrequests = []; /** @var int The number of data request to be displayed per page. */ protected $perpage; /** @var int[] The available options for the number of data request to be displayed per page. */ protected $perpageoptions = [25, 50, 100, 250]; /** * data_requests_table constructor. * * @param int $userid The user ID * @param int[] $statuses * @param int[] $types * @param int[] $creationmethods * @param bool $manage * @throws coding_exception */ public function __construct($userid = 0, $statuses = [], $types = [], $creationmethods = [], $manage = false) { parent::__construct('data-requests-table'); $this->userid = $userid; $this->statuses = $statuses; $this->types = $types; $this->creationmethods = $creationmethods; $this->manage = $manage; $checkboxattrs = [ 'title' => get_string('selectall'), 'data-action' => 'selectall' ]; $columnheaders = [ 'select' => html_writer::checkbox('selectall', 1, false, null, $checkboxattrs), 'type' => get_string('requesttype', 'tool_dataprivacy'), 'userid' => get_string('user', 'tool_dataprivacy'), 'timecreated' => get_string('daterequested', 'tool_dataprivacy'), 'requestedby' => get_string('requestby', 'tool_dataprivacy'), 'status' => get_string('requeststatus', 'tool_dataprivacy'), 'comments' => get_string('message', 'tool_dataprivacy'), 'actions' => '', ]; $this->define_columns(array_keys($columnheaders)); $this->define_headers(array_values($columnheaders)); $this->no_sorting('select', 'actions'); } /** * The select column. * * @param stdClass $data The row data. * @return string * @throws \moodle_exception * @throws coding_exception */ public function col_select($data) { if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) { if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE && !api::can_create_data_deletion_request_for_other()) { // Don't show checkbox if request's type is delete and user don't have permission. return false; } $stringdata = [ 'username' => $data->foruser->fullname, 'requesttype' => \core_text::strtolower($data->typenameshort) ]; return \html_writer::checkbox('requestids[]', $data->id, false, '', ['class' => 'selectrequests', 'title' => get_string('selectuserdatarequest', 'tool_dataprivacy', $stringdata)]); } } /** * The type column. * * @param stdClass $data The row data. * @return string */ public function col_type($data) { if ($this->manage) { return $data->typenameshort; } return $data->typename; } /** * The user column. * * @param stdClass $data The row data. * @return mixed */ public function col_userid($data) { $user = $data->foruser; return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]); } /** * The context information column. * * @param stdClass $data The row data. * @return string */ public function col_timecreated($data) { return userdate($data->timecreated); } /** * The requesting user's column. * * @param stdClass $data The row data. * @return mixed */ public function col_requestedby($data) { $user = $data->requestedbyuser; return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]); } /** * The status column. * * @param stdClass $data The row data. * @return mixed */ public function col_status($data) { return html_writer::span($data->statuslabel, 'badge ' . $data->statuslabelclass); } /** * The comments column. * * @param stdClass $data The row data. * @return string */ public function col_comments($data) { return shorten_text($data->comments, 60); } /** * The actions column. * * @param stdClass $data The row data. * @return string */ public function col_actions($data) { global $OUTPUT; $requestid = $data->id; $status = $data->status; $persistent = $this->datarequests[$requestid]; // Prepare actions. $actions = []; // View action. $actionurl = new moodle_url('#'); $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid]; $actiontext = get_string('viewrequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); switch ($status) { case api::DATAREQUEST_STATUS_PENDING: // Add action to mark a general enquiry request as complete. if ($data->type == api::DATAREQUEST_TYPE_OTHERS) { $actiondata['data-action'] = 'complete'; $nameemail = (object)[ 'name' => $data->foruser->fullname, 'email' => $data->foruser->email ]; $actiondata['data-requestid'] = $data->id; $actiondata['data-replytoemail'] = get_string('nameemail', 'tool_dataprivacy', $nameemail); $actiontext = get_string('markcomplete', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); } break; case api::DATAREQUEST_STATUS_AWAITING_APPROVAL: // Only show "Approve" and "Deny" button for deletion request if current user has permission. if ($persistent->get('type') == api::DATAREQUEST_TYPE_DELETE && !api::can_create_data_deletion_request_for_other()) { break; } // Approve. $actiondata['data-action'] = 'approve'; $actiontext = get_string('approverequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); // Deny. $actiondata['data-action'] = 'deny'; $actiontext = get_string('denyrequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); break; case api::DATAREQUEST_STATUS_DOWNLOAD_READY: $userid = $data->foruser->id; $usercontext = \context_user::instance($userid, IGNORE_MISSING); // If user has permission to view download link, show relevant action item. if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) { $actions[] = api::get_download_link($usercontext, $requestid); } break; } if ($this->manage) { $canreset = $persistent->is_active() || empty($this->ongoingrequests[$data->foruser->id]->{$data->type}); $canreset = $canreset && $persistent->is_resettable(); // Prevent re-submmit deletion request if current user don't have permission. $canreset = $canreset && ($persistent->get('type') != api::DATAREQUEST_TYPE_DELETE || api::can_create_data_deletion_request_for_other()); if ($canreset) { $reseturl = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', [ 'requestid' => $requestid, ]); $actiondata = ['data-action' => 'reset', 'data-requestid' => $requestid]; $actiontext = get_string('resubmitrequestasnew', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($reseturl, null, $actiontext, $actiondata); } } $actionsmenu = new action_menu($actions); $actionsmenu->set_menu_trigger(get_string('actions')); $actionsmenu->set_owner_selector('request-actions-' . $requestid); $actionsmenu->set_constraint('[data-region=data-requests-table] > .no-overflow'); return $OUTPUT->render($actionsmenu); } /** * Query the database for results to display in the table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. * @throws dml_exception * @throws coding_exception */ public function query_db($pagesize, $useinitialsbar = true) { global $PAGE; // Set dummy page total until we fetch full result set. $this->pagesize($pagesize, $pagesize + 1); $sort = $this->get_sql_sort(); // Get data requests from the given conditions. $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $this->creationmethods, $sort, $this->get_page_start(), $this->get_page_size()); // Count data requests from the given conditions. $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types, $this->creationmethods); $this->pagesize($pagesize, $total); $this->rawdata = []; $context = \context_system::instance(); $renderer = $PAGE->get_renderer('tool_dataprivacy'); $forusers = []; foreach ($datarequests as $persistent) { $this->datarequests[$persistent->get('id')] = $persistent; $exporter = new data_request_exporter($persistent, ['context' => $context]); $this->rawdata[] = $exporter->export($renderer); $forusers[] = $persistent->get('userid'); } // Fetch the list of all ongoing requests for the users currently shown. // This is used to determine whether any non-active request can be resubmitted. // There can only be one ongoing request of a type for each user. $this->ongoingrequests = api::find_ongoing_request_types_for_users($forusers); // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } } /** * Override default implementation to display a more meaningful information to the user. */ public function print_nothing_to_display() { global $OUTPUT; echo $this->render_reset_button(); $this->print_initials_bar(); if (!empty($this->statuses) || !empty($this->types)) { $message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy'); } else { $message = get_string('nodatarequests', 'tool_dataprivacy'); } echo $OUTPUT->notification($message, 'warning'); } /** * Override the table's show_hide_link method to prevent the show/hide links from rendering. * * @param string $column the column name, index into various names. * @param int $index numerical index of the column. * @return string HTML fragment. */ protected function show_hide_link($column, $index) { return ''; } /** * Override the table's wrap_html_finish method in order to render the bulk actions and * records per page options. */ public function wrap_html_finish() { global $OUTPUT; $data = new stdClass(); $data->options = [ [ 'value' => 0, 'name' => '' ], [ 'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_APPROVE, 'name' => get_string('approve', 'tool_dataprivacy') ], [ 'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_REJECT, 'name' => get_string('deny', 'tool_dataprivacy') ] ]; $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions); $perpageselect = new \single_select(new moodle_url(''), 'perpage', $perpageoptions, get_user_preferences('tool_dataprivacy_request-perpage'), null, 'selectgroup'); $perpageselect->label = get_string('perpage', 'moodle'); $data->perpage = $OUTPUT->render($perpageselect); echo $OUTPUT->render_from_template('tool_dataprivacy/data_requests_bulk_actions', $data); } /** * Set the number of data request records to be displayed per page. * * @param int $perpage The number of data request records. */ public function set_requests_per_page(int $perpage) { $this->perpage = $perpage; } /** * Get the number of data request records to be displayed per page. * * @return int The number of data request records. */ public function get_requests_per_page() : int { return $this->perpage; } /** * Set the available options for the number of data request to be displayed per page. * * @param array $perpageoptions The available options for the number of data request to be displayed per page. */ public function set_requests_per_page_options(array $perpageoptions) { $this->$perpageoptions = $perpageoptions; } /** * Get the available options for the number of data request to be displayed per page. * * @return array The available options for the number of data request to be displayed per page. */ public function get_requests_per_page_options() : array { return $this->perpageoptions; } } output/data_deletion_page.php 0000644 00000006113 15152024213 0012401 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for a user's data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use coding_exception; use moodle_exception; use moodle_url; use renderable; use renderer_base; use single_select; use stdClass; use templatable; use tool_dataprivacy\data_request; use tool_dataprivacy\local\helper; /** * Class containing data for a user's data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_deletion_page implements renderable, templatable { /** @var data_request[] $requests List of data requests. */ protected $filter = null; /** @var data_request[] $requests List of data requests. */ protected $expiredcontextstable = []; /** * Construct this renderable. * * @param \tool_dataprivacy\data_request[] $filter * @param expired_contexts_table $expiredcontextstable */ public function __construct($filter, expired_contexts_table $expiredcontextstable) { $this->filter = $filter; $this->expiredcontextstable = $expiredcontextstable; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php'); $options = [ CONTEXT_USER => get_string('user'), CONTEXT_COURSE => get_string('course'), CONTEXT_MODULE => get_string('activitiesandresources', 'tool_dataprivacy'), CONTEXT_BLOCK => get_string('blocks'), ]; $filterselector = new single_select($url, 'filter', $options, $this->filter, null); $data->filter = $filterselector->export_for_template($output); ob_start(); $this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true); $expiredcontexts = ob_get_contents(); ob_end_clean(); $data->expiredcontexts = $expiredcontexts; $data->existingcontexts = $this->expiredcontextstable->rawdata ? true : false; return $data; } } output/data_requests_page.php 0000644 00000006056 15152024213 0012457 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for a user's data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use coding_exception; use dml_exception; use moodle_exception; use moodle_url; use renderable; use renderer_base; use single_select; use stdClass; use templatable; use tool_dataprivacy\api; use tool_dataprivacy\local\helper; /** * Class containing data for a user's data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_requests_page implements renderable, templatable { /** @var data_requests_table $table The data requests table. */ protected $table; /** @var int[] $filters The applied filters. */ protected $filters = []; /** * Construct this renderable. * * @param data_requests_table $table The data requests table. * @param int[] $filters The applied filters. */ public function __construct($table, $filters) { $this->table = $table; $this->filters = $filters; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws dml_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $data->newdatarequesturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php'); $data->newdatarequesturl->param('manage', true); if (!is_https()) { $httpwarningmessage = get_string('httpwarning', 'tool_dataprivacy'); $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1); } $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); $filteroptions = helper::get_request_filter_options(); $filter = new request_filter($filteroptions, $this->filters, $url); $data->filter = $filter->export_for_template($output); ob_start(); $this->table->out($this->table->get_requests_per_page(), true); $requests = ob_get_contents(); ob_end_clean(); $data->datarequests = $requests; return $data; } } output/defaults_page.php 0000644 00000014736 15152024213 0011426 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for the data registry defaults. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use action_menu_link_primary; use coding_exception; use moodle_exception; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\data_registry; use tool_dataprivacy\external\category_exporter; use tool_dataprivacy\external\purpose_exporter; /** * Class containing data for the data registry defaults. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class defaults_page implements renderable, templatable { /** @var int $mode The display mode. */ protected $mode = null; /** @var int $category The default category for the given mode. */ protected $category = null; /** @var int $purpose The default purpose for the given mode. */ protected $purpose = null; /** @var stdClass[] $otherdefaults Other defaults for the given mode. */ protected $otherdefaults = []; /** @var bool $canedit Whether editing is allowed. */ protected $canedit = false; /** * Construct this renderable. * * @param int $mode The display mode. * @param int $category The default category for the given mode. * @param int $purpose The default purpose for the given mode. * @param stdClass[] $otherdefaults Other defaults for the given mode. * @param bool $canedit Whether editing is allowed. */ public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) { $this->mode = $mode; $this->category = $category; $this->purpose = $purpose; $this->otherdefaults = $otherdefaults; $this->canedit = $canedit; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { $data = new stdClass(); // Set tab URLs. $coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]); $courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]); $moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]); $blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]); $data->coursecaturl = $coursecaturl; $data->courseurl = $courseurl; $data->moduleurl = $moduleurl; $data->blockurl = $blockurl; // Set display mode. switch ($this->mode) { case CONTEXT_COURSECAT: $data->modecoursecat = true; break; case CONTEXT_COURSE: $data->modecourse = true; break; case CONTEXT_MODULE: $data->modemodule = true; break; case CONTEXT_BLOCK: $data->modeblock = true; break; default: $data->modecoursecat = true; break; } // Set config variables. $configname = \context_helper::get_class_for_level($this->mode); list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname); $data->categoryvar = $categoryvar; $data->purposevar = $purposevar; // Set default category. $data->categoryid = $this->category; $data->category = category_exporter::get_name($this->category); // Set default purpose. $data->purposeid = $this->purpose; $data->purpose = purpose_exporter::get_name($this->purpose); // Set other defaults. $otherdefaults = []; $url = new moodle_url('#'); foreach ($this->otherdefaults as $pluginname => $values) { $defaults = [ 'name' => $values->name, 'category' => category_exporter::get_name($values->category), 'purpose' => purpose_exporter::get_name($values->purpose), ]; if ($this->canedit) { $actions = []; // Edit link. $editattrs = [ 'data-action' => 'edit-activity-defaults', 'data-contextlevel' => $this->mode, 'data-activityname' => $pluginname, 'data-category' => $values->category, 'data-purpose' => $values->purpose, ]; $editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'), $editattrs); $actions[] = $editlink->export_for_template($output); // Delete link. $deleteattrs = [ 'data-action' => 'delete-activity-defaults', 'data-contextlevel' => $this->mode, 'data-activityname' => $pluginname, 'data-activitydisplayname' => $values->name, ]; $deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')), get_string('delete'), $deleteattrs); $actions[] = $deletelink->export_for_template($output); $defaults['actions'] = $actions; } $otherdefaults[] = (object)$defaults; } $data->otherdefaults = $otherdefaults; $data->canedit = $this->canedit; $data->contextlevel = $this->mode; return $data; } } output/request_filter.php 0000644 00000006544 15152024213 0011656 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing the filter options data for rendering the autocomplete element for the data requests page. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; defined('MOODLE_INTERNAL') || die(); /** * Class containing the filter options data for rendering the autocomplete element for the data requests page. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class request_filter implements renderable, templatable { /** @var array $filteroptions The filter options. */ protected $filteroptions; /** @var array $selectedoptions The list of selected filter option values. */ protected $selectedoptions; /** @var moodle_url|string $baseurl The url with params needed to call up this page. */ protected $baseurl; /** * request_filter constructor. * * @param array $filteroptions The filter options. * @param array $selectedoptions The list of selected filter option values. * @param string|moodle_url $baseurl The url with params needed to call up this page. */ public function __construct($filteroptions, $selectedoptions, $baseurl = null) { $this->filteroptions = $filteroptions; $this->selectedoptions = $selectedoptions; if (!empty($baseurl)) { $this->baseurl = new moodle_url($baseurl); } } /** * Function to export the renderer data in a format that is suitable for a mustache template. * * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. * @return stdClass|array */ public function export_for_template(renderer_base $output) { global $PAGE; $data = new stdClass(); if (empty($this->baseurl)) { $this->baseurl = $PAGE->url; } $data->action = $this->baseurl->out(false); foreach ($this->selectedoptions as $option) { if (!isset($this->filteroptions[$option])) { $this->filteroptions[$option] = $option; } } $data->filteroptions = []; foreach ($this->filteroptions as $value => $label) { $selected = in_array($value, $this->selectedoptions); $filteroption = (object)[ 'value' => $value, 'label' => $label ]; $filteroption->selected = $selected; $data->filteroptions[] = $filteroption; } return $data; } } output/my_data_requests_page.php 0000644 00000014205 15152024213 0013157 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing data for a user's data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use action_menu; use action_menu_link_secondary; use coding_exception; use context_user; use moodle_exception; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\api; use tool_dataprivacy\data_request; use tool_dataprivacy\external\data_request_exporter; /** * Class containing data for a user's data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class my_data_requests_page implements renderable, templatable { /** @var array $requests List of data requests. */ protected $requests = []; /** * Construct this renderable. * * @param data_request[] $requests */ public function __construct($requests) { $this->requests = $requests; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { global $USER; $data = new stdClass(); $data->newdatarequesturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php'); if (!is_https()) { $httpwarningmessage = get_string('httpwarning', 'tool_dataprivacy'); $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1); } $requests = []; foreach ($this->requests as $request) { $requestid = $request->get('id'); $status = $request->get('status'); $userid = $request->get('userid'); $type = $request->get('type'); $usercontext = context_user::instance($userid, IGNORE_MISSING); if (!$usercontext) { // Use the context system. $outputcontext = \context_system::instance(); } else { $outputcontext = $usercontext; } $requestexporter = new data_request_exporter($request, ['context' => $outputcontext]); $item = $requestexporter->export($output); $self = $request->get('userid') == $USER->id; if (!$self) { // Append user name if it differs from $USER. $a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname]; $item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a); } $candownload = false; $cancancel = true; switch ($status) { case api::DATAREQUEST_STATUS_COMPLETE: $item->statuslabelclass = 'badge-success'; $item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy'); $cancancel = false; break; case api::DATAREQUEST_STATUS_DOWNLOAD_READY: $item->statuslabelclass = 'badge-success'; $item->statuslabel = get_string('statusready', 'tool_dataprivacy'); $cancancel = false; $candownload = true; if ($usercontext) { $candownload = api::can_download_data_request_for_user( $request->get('userid'), $request->get('requestedby')); } break; case api::DATAREQUEST_STATUS_DELETED: $item->statuslabelclass = 'badge-success'; $item->statuslabel = get_string('statusdeleted', 'tool_dataprivacy'); $cancancel = false; break; case api::DATAREQUEST_STATUS_EXPIRED: $item->statuslabelclass = 'badge-secondary'; $item->statuslabel = get_string('statusexpired', 'tool_dataprivacy'); $item->statuslabeltitle = get_string('downloadexpireduser', 'tool_dataprivacy'); $cancancel = false; break; case api::DATAREQUEST_STATUS_CANCELLED: case api::DATAREQUEST_STATUS_REJECTED: $cancancel = false; break; } // Prepare actions. $actions = []; if ($cancancel) { $cancelurl = new moodle_url('#'); $canceldata = ['data-action' => 'cancel', 'data-requestid' => $requestid]; $canceltext = get_string('cancelrequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata); } if ($candownload && $usercontext) { $actions[] = api::get_download_link($usercontext, $requestid); } if (!empty($actions)) { $actionsmenu = new action_menu($actions); $actionsmenu->set_menu_trigger(get_string('actions')); $actionsmenu->set_owner_selector('request-actions-' . $requestid); $item->actions = $actionsmenu->export_for_template($output); } $requests[] = $item; } $data->requests = $requests; return $data; } } output/crud_element.php 0000644 00000006055 15152024213 0011264 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Abstract renderer for independent renderable elements. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\external\purpose_exporter; use tool_dataprivacy\external\category_exporter; /** * Abstract renderer for independent renderable elements. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class crud_element { /** * Returns the top navigation buttons. * * @return \action_link[] */ protected final function get_navigation() { $back = new \action_link( new \moodle_url('/admin/tool/dataprivacy/dataregistry.php'), get_string('back'), null, ['class' => 'btn btn-primary'] ); return [$back]; } /** * Adds an action menu for the provided element * * @param string $elementname 'purpose' or 'category'. * @param \stdClass $exported * @param \core\persistent $persistent * @return \action_menu */ protected final function action_menu($elementname, $exported, $persistent) { // Just in case, we are doing funny stuff below. $elementname = clean_param($elementname, PARAM_ALPHA); // Actions. $actionmenu = new \action_menu(); $actionmenu->set_menu_trigger(get_string('actions')); $actionmenu->set_owner_selector($elementname . '-' . $exported->id . '-actions'); $url = new \moodle_url('/admin/tool/dataprivacy/edit' . $elementname . '.php', ['id' => $exported->id]); $link = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit')); $actionmenu->add($link); if (!$persistent->is_used()) { $url = new \moodle_url('#'); $attrs = ['data-id' => $exported->id, 'data-action' => 'delete' . $elementname, 'data-name' => $exported->name]; $link = new \action_menu_link_secondary($url, new \pix_icon('t/delete', get_string('delete')), get_string('delete'), $attrs); $actionmenu->add($link); } return $actionmenu; } } output/summary_page.php 0000644 00000012267 15152024213 0011311 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Summary page renderable. * * @package tool_dataprivacy * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; /** * Class containing the summary page renderable. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class summary_page implements renderable, templatable { /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return array */ public function export_for_template(renderer_base $output) { $contextlevels = [ 'contextlevelname10' => CONTEXT_SYSTEM, 'contextlevelname30' => CONTEXT_USER, 'contextlevelname40' => CONTEXT_COURSECAT, 'contextlevelname50' => CONTEXT_COURSE, 'contextlevelname70' => CONTEXT_MODULE, 'contextlevelname80' => CONTEXT_BLOCK ]; $data = []; $context = \context_system::instance(); foreach ($contextlevels as $levelname => $level) { $classname = \context_helper::get_class_for_level($level); list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); $purposeid = get_config('tool_dataprivacy', $purposevar); $categoryid = get_config('tool_dataprivacy', $categoryvar); $section = []; $section['contextname'] = get_string($levelname, 'tool_dataprivacy'); if (empty($purposeid)) { list($purposeid, $categoryid) = \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level); } if ($purposeid == -1) { $purposeid = 0; } $purpose = new \tool_dataprivacy\purpose($purposeid); $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]); $purposedata = $export->export($output); $section['purpose'] = $purposedata; if (empty($categoryid)) { list($purposeid, $categoryid) = \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level); } if ($categoryid == -1) { $categoryid = 0; } $category = new \tool_dataprivacy\category($categoryid); $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]); $categorydata = $export->export($output); $section['category'] = $categorydata; $data['contexts'][] = $section; } // Get activity module plugin info. $pluginmanager = \core_plugin_manager::instance(); $modplugins = $pluginmanager->get_enabled_plugins('mod'); foreach ($modplugins as $name) { $classname = \context_helper::get_class_for_level($contextlevels['contextlevelname70']); list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name); $categoryid = get_config('tool_dataprivacy', $categoryvar); $purposeid = get_config('tool_dataprivacy', $purposevar); if ($categoryid === false && $purposeid === false) { // If no purpose and category has been set for this plugin, then there's no need to show this on the list. continue; } $section = []; $section['contextname'] = $pluginmanager->plugin_name('mod_' . $name); if ($purposeid == -1) { $purposeid = 0; } $purpose = new \tool_dataprivacy\purpose($purposeid); $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]); $purposedata = $export->export($output); $section['purpose'] = $purposedata; if ($categoryid == -1) { $categoryid = 0; } $category = new \tool_dataprivacy\category($categoryid); $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]); $categorydata = $export->export($output); $section['category'] = $categorydata; $data['contexts'][] = $section; } return $data; } } output/data_registry_page.php 0000644 00000037010 15152024213 0012446 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Data registry renderable. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\data_registry; require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); require_once($CFG->libdir . '/blocklib.php'); /** * Class containing the data registry renderable * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_registry_page implements renderable, templatable { /** * @var int */ private $defaultcontextlevel; /** * @var int */ private $defaultcontextid; /** * Constructor. * * @param int $defaultcontextlevel * @param int $defaultcontextid * @return null */ public function __construct($defaultcontextlevel = false, $defaultcontextid = false) { $this->defaultcontextlevel = $defaultcontextlevel; $this->defaultcontextid = $defaultcontextid; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $PAGE; $params = [\context_system::instance()->id, $this->defaultcontextlevel, $this->defaultcontextid]; $PAGE->requires->js_call_amd('tool_dataprivacy/data_registry', 'init', $params); $data = new stdClass(); $defaultsbutton = new \action_link( new \moodle_url('/admin/tool/dataprivacy/defaults.php'), get_string('setdefaults', 'tool_dataprivacy'), null, ['class' => 'btn btn-primary'] ); $data->defaultsbutton = $defaultsbutton->export_for_template($output); $actionmenu = new \action_menu(); $actionmenu->set_menu_trigger(get_string('edit'), 'btn btn-primary'); $actionmenu->set_owner_selector('dataregistry-actions'); $url = new \moodle_url('/admin/tool/dataprivacy/categories.php'); $categories = new \action_menu_link_secondary($url, null, get_string('categories', 'tool_dataprivacy')); $actionmenu->add($categories); $url = new \moodle_url('/admin/tool/dataprivacy/purposes.php'); $purposes = new \action_menu_link_secondary($url, null, get_string('purposes', 'tool_dataprivacy')); $actionmenu->add($purposes); $data->actions = $actionmenu->export_for_template($output); if (!data_registry::defaults_set()) { $data->info = (object)[ 'message' => get_string('dataregistryinfo', 'tool_dataprivacy'), 'announce' => 1 ]; $data->nosystemdefaults = (object)[ 'message' => get_string('nosystemdefaults', 'tool_dataprivacy'), 'announce' => 1 ]; } $data->tree = $this->get_default_tree_structure(); return $data; } /** * Returns the tree default structure. * * @return array */ private function get_default_tree_structure() { $frontpage = \context_course::instance(SITEID); $categorybranches = $this->get_all_category_branches(); $elements = [ 'text' => get_string('contextlevelname' . CONTEXT_SYSTEM, 'tool_dataprivacy'), 'contextlevel' => CONTEXT_SYSTEM, 'branches' => [ [ 'text' => get_string('user'), 'contextlevel' => CONTEXT_USER, ], [ 'text' => get_string('categories'), 'branches' => $categorybranches, 'expandelement' => 'category', ], [ 'text' => get_string('frontpagecourse', 'tool_dataprivacy'), 'contextid' => $frontpage->id, 'branches' => [ [ 'text' => get_string('activitiesandresources', 'tool_dataprivacy'), 'expandcontextid' => $frontpage->id, 'expandelement' => 'module', 'expanded' => 0, ], [ 'text' => get_string('blocks'), 'expandcontextid' => $frontpage->id, 'expandelement' => 'block', 'expanded' => 0, ], ] ] ] ]; // Returned as an array to follow a common array format. return [self::complete($elements, $this->defaultcontextlevel, $this->defaultcontextid)]; } /** * Returns the hierarchy of system course categories. * * @return array */ private function get_all_category_branches() { $categories = data_registry::get_site_categories(); $categoriesbranch = []; while (count($categories) > 0) { foreach ($categories as $key => $category) { $context = \context_coursecat::instance($category->id); $newnode = [ 'text' => shorten_text(format_string($category->name, true, ['context' => $context])), 'categoryid' => $category->id, 'contextid' => $context->id, ]; if ($category->coursecount > 0) { $newnode['branches'] = [ [ 'text' => get_string('courses'), 'expandcontextid' => $context->id, 'expandelement' => 'course', 'expanded' => 0, ] ]; } $added = false; if ($category->parent == 0) { // New categories root-level node. $categoriesbranch[] = $newnode; $added = true; } else { // Add the new node under the appropriate parent. if ($this->add_to_parent_category_branch($category, $newnode, $categoriesbranch)) { $added = true; } } if ($added) { unset($categories[$key]); } } } return $categoriesbranch; } /** * Gets the courses branch for the provided category. * * @param \context $catcontext * @return array */ public static function get_courses_branch(\context $catcontext) { if ($catcontext->contextlevel !== CONTEXT_COURSECAT) { throw new \coding_exception('A course category context should be provided'); } $coursecat = \core_course_category::get($catcontext->instanceid); $courses = $coursecat->get_courses(); $branches = []; foreach ($courses as $course) { $coursecontext = \context_course::instance($course->id); $coursenode = [ 'text' => shorten_text(format_string($course->shortname, true, ['context' => $coursecontext])), 'contextid' => $coursecontext->id, 'branches' => [ [ 'text' => get_string('activitiesandresources', 'tool_dataprivacy'), 'expandcontextid' => $coursecontext->id, 'expandelement' => 'module', 'expanded' => 0, ], [ 'text' => get_string('blocks'), 'expandcontextid' => $coursecontext->id, 'expandelement' => 'block', 'expanded' => 0, ], ] ]; $branches[] = self::complete($coursenode); } return $branches; } /** * Gets the modules branch for the provided course. * * @param \context $coursecontext * @return array */ public static function get_modules_branch(\context $coursecontext) { if ($coursecontext->contextlevel !== CONTEXT_COURSE) { throw new \coding_exception('A course context should be provided'); } $branches = []; // Using the current user. $modinfo = get_fast_modinfo($coursecontext->instanceid); foreach ($modinfo->get_instances() as $moduletype => $instances) { foreach ($instances as $cm) { if (!$cm->uservisible) { continue; } $a = (object)[ 'instancename' => shorten_text($cm->get_formatted_name()), 'modulename' => get_string('pluginname', 'mod_' . $moduletype), ]; $text = get_string('moduleinstancename', 'tool_dataprivacy', $a); $branches[] = self::complete([ 'text' => $text, 'contextid' => $cm->context->id, ]); } } return $branches; } /** * Gets the blocks branch for the provided course. * * @param \context $coursecontext * @return null */ public static function get_blocks_branch(\context $coursecontext) { global $DB; if ($coursecontext->contextlevel !== CONTEXT_COURSE) { throw new \coding_exception('A course context should be provided'); } $branches = []; $children = $coursecontext->get_child_contexts(); foreach ($children as $childcontext) { if ($childcontext->contextlevel !== CONTEXT_BLOCK) { continue; } $blockinstance = block_instance_by_id($childcontext->instanceid); $displayname = shorten_text(format_string($blockinstance->get_title(), true, ['context' => $childcontext])); $branches[] = self::complete([ 'text' => $displayname, 'contextid' => $childcontext->id, ]); } return $branches; } /** * Adds the provided category to the categories branch. * * @param stdClass $category * @param array $newnode * @param array $categoriesbranch * @return bool */ private function add_to_parent_category_branch($category, $newnode, &$categoriesbranch) { foreach ($categoriesbranch as $key => $branch) { if (!empty($branch['categoryid']) && $branch['categoryid'] == $category->parent) { // It may be empty (if it does not contain courses and this is the first child cat). if (!isset($categoriesbranch[$key]['branches'])) { $categoriesbranch[$key]['branches'] = []; } $categoriesbranch[$key]['branches'][] = $newnode; return true; } if (!empty($branch['branches'])) { $parent = $this->add_to_parent_category_branch($category, $newnode, $categoriesbranch[$key]['branches']); if ($parent) { return true; } } } return false; } /** * Completes tree nodes with default values. * * @param array $node * @param int|false $currentcontextlevel * @param int|false $currentcontextid * @return array */ private static function complete($node, $currentcontextlevel = false, $currentcontextid = false) { if (!isset($node['active'])) { if ($currentcontextlevel && !empty($node['contextlevel']) && $currentcontextlevel == $node['contextlevel'] && empty($currentcontextid)) { // This is the active context level, we also checked that there // is no default contextid set. $node['active'] = true; } else if ($currentcontextid && !empty($node['contextid']) && $currentcontextid == $node['contextid']) { $node['active'] = true; } else { $node['active'] = null; } } if (!isset($node['branches'])) { $node['branches'] = []; } else { foreach ($node['branches'] as $key => $childnode) { $node['branches'][$key] = self::complete($childnode, $currentcontextlevel, $currentcontextid); } } if (!isset($node['expandelement'])) { $node['expandelement'] = null; } if (!isset($node['expandcontextid'])) { $node['expandcontextid'] = null; } if (!isset($node['contextid'])) { $node['contextid'] = null; } if (!isset($node['contextlevel'])) { $node['contextlevel'] = null; } if (!isset($node['expanded'])) { if (!empty($node['branches'])) { $node['expanded'] = 1; } else { $node['expanded'] = 0; } } return $node; } /** * From a list of purpose persistents to a list of id => name purposes. * * @param \tool_dataprivacy\purpose[] $purposes * @param bool $includenotset * @param bool $includeinherit * @return string[] */ public static function purpose_options($purposes, $includenotset = true, $includeinherit = true) { $options = self::base_options($includenotset, $includeinherit); foreach ($purposes as $purpose) { $options[$purpose->get('id')] = $purpose->get('name'); } return $options; } /** * From a list of category persistents to a list of id => name categories. * * @param \tool_dataprivacy\category[] $categories * @param bool $includenotset * @param bool $includeinherit * @return string[] */ public static function category_options($categories, $includenotset = true, $includeinherit = true) { $options = self::base_options($includenotset, $includeinherit); foreach ($categories as $category) { $options[$category->get('id')] = $category->get('name'); } return $options; } /** * Base not set and inherit options. * * @param bool $includenotset * @param bool $includeinherit * @return array */ private static function base_options($includenotset = true, $includeinherit = true) { $options = []; if ($includenotset) { $options[\tool_dataprivacy\context_instance::NOTSET] = get_string('notset', 'tool_dataprivacy'); } if ($includeinherit) { $options[\tool_dataprivacy\context_instance::INHERIT] = get_string('inherit', 'tool_dataprivacy'); } return $options; } } output/expired_contexts_table.php 0000644 00000034226 15152024213 0013355 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the class used for the displaying the expired contexts table. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); use coding_exception; use context_helper; use dml_exception; use Exception; use html_writer; use pix_icon; use stdClass; use table_sql; use tool_dataprivacy\api; use tool_dataprivacy\expired_context; use tool_dataprivacy\external\purpose_exporter; use tool_dataprivacy\purpose; defined('MOODLE_INTERNAL') || die; /** * The class for displaying the expired contexts table. * * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_contexts_table extends table_sql { /** @var int The context level acting as a filter for this table. */ protected $contextlevel = null; /** * @var bool $selectall Has the user selected all users on the page? True by default. */ protected $selectall = true; /** @var purpose[] Array of purposes by their id. */ protected $purposes = []; /** @var purpose[] Map of context => purpose. */ protected $purposemap = []; /** @var array List of roles. */ protected $roles = []; /** * expired_contexts_table constructor. * * @param int|null $contextlevel * @throws coding_exception */ public function __construct($contextlevel = null) { parent::__construct('expired-contexts-table'); $this->contextlevel = $contextlevel; $columnheaders = [ 'name' => get_string('name'), 'info' => get_string('info'), 'purpose' => get_string('purpose', 'tool_dataprivacy'), 'category' => get_string('category', 'tool_dataprivacy'), 'retentionperiod' => get_string('retentionperiod', 'tool_dataprivacy'), 'tobedeleted' => get_string('tobedeleted', 'tool_dataprivacy'), 'timecreated' => get_string('expiry', 'tool_dataprivacy'), ]; $checkboxattrs = [ 'title' => get_string('selectall'), 'data-action' => 'selectall' ]; $columnheaders['select'] = html_writer::checkbox('selectall', 1, true, null, $checkboxattrs); $this->define_columns(array_keys($columnheaders)); $this->define_headers(array_values($columnheaders)); $this->no_sorting('name'); $this->no_sorting('select'); $this->no_sorting('info'); $this->no_sorting('purpose'); $this->no_sorting('category'); $this->no_sorting('retentionperiod'); $this->no_sorting('tobedeleted'); // Make this table sorted by first name by default. $this->sortable(true, 'timecreated'); // We use roles in several places. $this->roles = role_get_names(); } /** * The context name column. * * @param stdClass $expiredctx The row data. * @return string * @throws coding_exception */ public function col_name($expiredctx) { global $OUTPUT; $context = context_helper::instance_by_id($expiredctx->get('contextid')); $parent = $context->get_parent_context(); $contextdata = (object)[ 'name' => $context->get_context_name(false, true), 'parent' => $parent->get_context_name(false, true), ]; $fullcontexts = $context->get_parent_contexts(true); $contextsinpath = []; foreach ($fullcontexts as $contextinpath) { $contextsinpath[] = $contextinpath->get_context_name(false, true); } $infoicon = new pix_icon('i/info', implode(' / ', array_reverse($contextsinpath))); $infoiconhtml = $OUTPUT->render($infoicon); $name = html_writer::span(get_string('nameandparent', 'tool_dataprivacy', $contextdata), 'mr-1'); return $name . $infoiconhtml; } /** * The context information column. * * @param stdClass $expiredctx The row data. * @return string * @throws coding_exception */ public function col_info($expiredctx) { global $OUTPUT; $context = context_helper::instance_by_id($expiredctx->get('contextid')); $children = $context->get_child_contexts(); if (empty($children)) { return get_string('none'); } else { $childnames = []; foreach ($children as $child) { $childnames[] = $child->get_context_name(false, true); } $infoicon = new pix_icon('i/info', implode(', ', $childnames)); $infoiconhtml = $OUTPUT->render($infoicon); $name = html_writer::span(get_string('nchildren', 'tool_dataprivacy', count($children)), 'mr-1'); return $name . $infoiconhtml; } } /** * The category name column. * * @param stdClass $expiredctx The row data. * @return mixed * @throws coding_exception * @throws dml_exception */ public function col_category($expiredctx) { $context = context_helper::instance_by_id($expiredctx->get('contextid')); $category = api::get_effective_context_category($context); return s($category->get('name')); } /** * The purpose column. * * @param stdClass $expiredctx The row data. * @return string * @throws coding_exception */ public function col_purpose($expiredctx) { $purpose = $this->get_purpose_for_expiry($expiredctx); return s($purpose->get('name')); } /** * The retention period column. * * @param stdClass $expiredctx The row data. * @return string */ public function col_retentionperiod($expiredctx) { $purpose = $this->get_purpose_for_expiry($expiredctx); $expiries = []; $expiry = html_writer::tag('dt', get_string('default'), ['class' => 'col-sm-3']); if ($expiredctx->get('defaultexpired')) { $expiries[get_string('default')] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))), ]); } else { $expiries[get_string('default')] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))), ]); } if (!$expiredctx->is_fully_expired()) { $purposeoverrides = $purpose->get_purpose_overrides(); foreach ($expiredctx->get('unexpiredroles') as $roleid) { $role = $this->roles[$roleid]; $override = $purposeoverrides[$roleid]; $expiries[$role->localname] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))), ]); } foreach ($expiredctx->get('expiredroles') as $roleid) { $role = $this->roles[$roleid]; $override = $purposeoverrides[$roleid]; $expiries[$role->localname] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))), ]); } } $output = array_map(function($rolename, $expiry) { $return = html_writer::tag('dt', $rolename, ['class' => 'col-sm-3']); $return .= html_writer::tag('dd', $expiry, ['class' => 'col-sm-9']); return $return; }, array_keys($expiries), $expiries); return html_writer::tag('dl', implode($output), ['class' => 'row']); } /** * The timecreated a.k.a. the context expiry date column. * * @param stdClass $expiredctx The row data. * @return string */ public function col_timecreated($expiredctx) { return userdate($expiredctx->get('timecreated')); } /** * Generate the select column. * * @param stdClass $expiredctx The row data. * @return string */ public function col_select($expiredctx) { $id = $expiredctx->get('id'); return html_writer::checkbox('expiredcontext_' . $id, $id, $this->selectall, '', ['class' => 'selectcontext']); } /** * Formatting for the 'tobedeleted' column which indicates in a friendlier fashion whose data will be removed. * * @param stdClass $expiredctx The row data. * @return string */ public function col_tobedeleted($expiredctx) { if ($expiredctx->is_fully_expired()) { return get_string('defaultexpired', 'tool_dataprivacy'); } $purpose = $this->get_purpose_for_expiry($expiredctx); $a = (object) []; $expiredroles = []; foreach ($expiredctx->get('expiredroles') as $roleid) { $expiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname); } $a->expired = html_writer::tag('ul', implode($expiredroles)); $unexpiredroles = []; foreach ($expiredctx->get('unexpiredroles') as $roleid) { $unexpiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname); } $a->unexpired = html_writer::tag('ul', implode($unexpiredroles)); if ($expiredctx->get('defaultexpired')) { return get_string('defaultexpiredexcept', 'tool_dataprivacy', $a); } else if (empty($unexpiredroles)) { return get_string('defaultunexpired', 'tool_dataprivacy', $a); } else { return get_string('defaultunexpiredwithexceptions', 'tool_dataprivacy', $a); } } /** * Query the database for results to display in the table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. * @throws dml_exception * @throws coding_exception */ public function query_db($pagesize, $useinitialsbar = true) { // Only count expired contexts that are awaiting confirmation. $total = expired_context::get_record_count_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED); $this->pagesize($pagesize, $total); $sort = $this->get_sql_sort(); if (empty($sort)) { $sort = 'timecreated'; } // Only load expired contexts that are awaiting confirmation. $expiredcontexts = expired_context::get_records_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED, $sort, $this->get_page_start(), $this->get_page_size()); $this->rawdata = []; $contextids = []; foreach ($expiredcontexts as $persistent) { $this->rawdata[] = $persistent; $contextids[] = $persistent->get('contextid'); } $this->preload_contexts($contextids); // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } } /** * Override default implementation to display a more meaningful information to the user. */ public function print_nothing_to_display() { global $OUTPUT; echo $this->render_reset_button(); $this->print_initials_bar(); echo $OUTPUT->notification(get_string('noexpiredcontexts', 'tool_dataprivacy'), 'warning'); } /** * Override the table's show_hide_link method to prevent the show/hide link for the select column from rendering. * * @param string $column the column name, index into various names. * @param int $index numerical index of the column. * @return string HTML fragment. */ protected function show_hide_link($column, $index) { if ($index < 6) { return parent::show_hide_link($column, $index); } return ''; } /** * Get the purpose for the specified expired context. * * @param expired_context $expiredcontext * @return purpose */ protected function get_purpose_for_expiry(expired_context $expiredcontext) : purpose { $context = context_helper::instance_by_id($expiredcontext->get('contextid')); if (empty($this->purposemap[$context->id])) { $purpose = api::get_effective_context_purpose($context); $this->purposemap[$context->id] = $purpose->get('id'); if (empty($this->purposes[$purpose->get('id')])) { $this->purposes[$purpose->get('id')] = $purpose; } } return $this->purposes[$this->purposemap[$context->id]]; } /** * Preload context records given a set of contextids. * * @param array $contextids */ protected function preload_contexts(array $contextids) { global $DB; if (empty($contextids)) { return; } $ctxfields = \context_helper::get_preload_record_columns_sql('ctx'); list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); $sql = "SELECT {$ctxfields} FROM {context} ctx WHERE ctx.id {$insql}"; $contextlist = $DB->get_recordset_sql($sql, $inparams); foreach ($contextlist as $contextdata) { \context_helper::preload_from_record($contextdata); } $contextlist->close(); } } output/data_registry_compliance_page.php 0000644 00000003416 15152024213 0014643 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the data registry compliance renderable. * * @package tool_dataprivacy * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; /** * Class containing the data registry compliance renderable * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_registry_compliance_page implements renderable, templatable { /** @var array meta-data to be displayed about the system. */ protected $metadata; /** * Constructor. * * @param array $metadata */ public function __construct($metadata) { $this->metadata = $metadata; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { return ['types' => $this->metadata]; } } data_request.php 0000644 00000023647 15152024213 0007745 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing data requests from the DB. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); use lang_string; use core\persistent; /** * Class for loading/storing data requests from the DB. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_request extends persistent { /** The table name this persistent object maps to. */ const TABLE = 'tool_dataprivacy_request'; /** Data request created manually. */ const DATAREQUEST_CREATION_MANUAL = 0; /** Data request created automatically. */ const DATAREQUEST_CREATION_AUTO = 1; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return [ 'type' => [ 'choices' => [ api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_TYPE_OTHERS, ], 'type' => PARAM_INT ], 'comments' => [ 'type' => PARAM_TEXT, 'message' => new lang_string('errorinvalidrequestcomments', 'tool_dataprivacy'), 'default' => '' ], 'commentsformat' => [ 'choices' => [ FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN ], 'type' => PARAM_INT, 'default' => FORMAT_PLAIN ], 'userid' => [ 'default' => function() { global $USER; return $USER->id; }, 'type' => PARAM_INT ], 'requestedby' => [ 'default' => 0, 'type' => PARAM_INT ], 'status' => [ 'default' => api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 'choices' => [ api::DATAREQUEST_STATUS_PENDING, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, api::DATAREQUEST_STATUS_APPROVED, api::DATAREQUEST_STATUS_PROCESSING, api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED, api::DATAREQUEST_STATUS_REJECTED, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_EXPIRED, api::DATAREQUEST_STATUS_DELETED, ], 'type' => PARAM_INT ], 'dpo' => [ 'default' => 0, 'type' => PARAM_INT, 'null' => NULL_ALLOWED ], 'dpocomment' => [ 'default' => '', 'type' => PARAM_TEXT, 'null' => NULL_ALLOWED ], 'dpocommentformat' => [ 'choices' => [ FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN ], 'type' => PARAM_INT, 'default' => FORMAT_PLAIN ], 'systemapproved' => [ 'default' => false, 'type' => PARAM_BOOL, ], 'creationmethod' => [ 'default' => self::DATAREQUEST_CREATION_MANUAL, 'choices' => [ self::DATAREQUEST_CREATION_MANUAL, self::DATAREQUEST_CREATION_AUTO ], 'type' => PARAM_INT ], ]; } /** * Determines whether a completed data export request has expired. * The response will be valid regardless of the expiry scheduled task having run. * * @param data_request $request the data request object whose expiry will be checked. * @return bool true if the request has expired. */ public static function is_expired(data_request $request) { $result = false; // Only export requests expire. if ($request->get('type') == api::DATAREQUEST_TYPE_EXPORT) { switch ($request->get('status')) { // Expired requests are obviously expired. case api::DATAREQUEST_STATUS_EXPIRED: $result = true; break; // Complete requests are expired if the expiry time has elapsed. case api::DATAREQUEST_STATUS_DOWNLOAD_READY: $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry'); if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) { $result = true; } break; } } return $result; } /** * Fetch completed data requests which are due to expire. * * @param int $userid Optional user ID to filter by. * * @return array Details of completed requests which are due to expire. */ public static function get_expired_requests($userid = 0) { global $DB; $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry'); $expirytime = strtotime("-{$expiryseconds} second"); $table = self::TABLE; $sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime'; $params = array( 'export_type' => api::DATAREQUEST_TYPE_EXPORT, 'completestatus' => api::DATAREQUEST_STATUS_DOWNLOAD_READY, 'expirytime' => $expirytime, ); $sort = 'id'; $fields = 'id, userid'; // Filter by user ID if specified. if ($userid > 0) { $sqlwhere .= ' AND (userid = :userid OR requestedby = :requestedby)'; $params['userid'] = $userid; $params['requestedby'] = $userid; } return $DB->get_records_select_menu($table, $sqlwhere, $params, $sort, $fields, 0, 2000); } /** * Expire a given set of data requests. * Update request status and delete the files. * * @param array $expiredrequests [requestid => userid] * * @return void */ public static function expire($expiredrequests) { global $DB; $ids = array_keys($expiredrequests); if (count($ids) > 0) { list($insql, $inparams) = $DB->get_in_or_equal($ids); $initialparams = array(api::DATAREQUEST_STATUS_EXPIRED, time()); $params = array_merge($initialparams, $inparams); $update = "UPDATE {" . self::TABLE . "} SET status = ?, timemodified = ? WHERE id $insql"; if ($DB->execute($update, $params)) { $fs = get_file_storage(); foreach ($expiredrequests as $id => $userid) { $usercontext = \context_user::instance($userid); $fs->delete_area_files($usercontext->id, 'tool_dataprivacy', 'export', $id); } } } } /** * Whether this request is in a state appropriate for reset/resubmission. * * Note: This does not check whether any other completed requests exist for this user. * * @return bool */ public function is_resettable() : bool { if (api::DATAREQUEST_TYPE_OTHERS == $this->get('type')) { // It is not possible to reset 'other' reqeusts. return false; } $resettable = [ api::DATAREQUEST_STATUS_APPROVED => true, api::DATAREQUEST_STATUS_REJECTED => true, ]; return isset($resettable[$this->get('status')]); } /** * Whether this request is 'active'. * * @return bool */ public function is_active() : bool { $active = [ api::DATAREQUEST_STATUS_APPROVED => true, ]; return isset($active[$this->get('status')]); } /** * Reject this request and resubmit it as a fresh request. * * Note: This does not check whether any other completed requests exist for this user. * * @return self */ public function resubmit_request() : data_request { if ($this->is_active()) { $this->set('status', api::DATAREQUEST_STATUS_REJECTED)->save(); } if (!$this->is_resettable()) { throw new \moodle_exception('cannotreset', 'tool_dataprivacy'); } $currentdata = $this->to_record(); unset($currentdata->id); // Clone the original request, but do not notify. $clone = api::create_data_request( $this->get('userid'), $this->get('type'), $this->get('comments'), $this->get('creationmethod'), false ); $clone->set('comments', $this->get('comments')); $clone->set('dpo', $this->get('dpo')); $clone->set('requestedby', $this->get('requestedby')); $clone->save(); return $clone; } } context_instance.php 0000644 00000006317 15152024213 0010627 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing context instances data from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * Class for loading/storing context instances data from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context_instance extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_ctxinstance'; /** * Not set value. */ const NOTSET = 0; /** * Inherit value. */ const INHERIT = -1; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'contextid' => array( 'type' => PARAM_INT, 'description' => 'The context id.', ), 'purposeid' => array( 'type' => PARAM_INT, 'description' => 'The purpose id.', 'null' => NULL_ALLOWED, ), 'categoryid' => array( 'type' => PARAM_INT, 'description' => 'The category id.', 'null' => NULL_ALLOWED, ), ); } /** * Returns an instance by contextid. * * @param mixed $contextid * @param mixed $exception * @return null */ public static function get_record_by_contextid($contextid, $exception = true) { global $DB; if (!$record = $DB->get_record(self::TABLE, array('contextid' => $contextid))) { if (!$exception) { return false; } else { throw new \dml_missing_record_exception(self::TABLE); } } return new static(0, $record); } /** * Is the provided purpose used by any context instance? * * @param int $purposeid * @return bool */ public static function is_purpose_used($purposeid) { global $DB; return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid)); } /** * Is the provided category used by any context instance? * * @param int $categoryid * @return bool */ public static function is_category_used($categoryid) { global $DB; return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid)); } } page_helper.php 0000644 00000006134 15152024213 0007527 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Page helper. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use context_system; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Page helper. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_helper { /** * Sets up $PAGE for data privacy admin pages. * * @param moodle_url $url The page URL. * @param string $title The page's title. * @param string $attachtoparentnode The parent navigation node where this page can be accessed from. * @param string $requiredcapability The required capability to view this page. */ public static function setup(moodle_url $url, $title, $attachtoparentnode = '', $requiredcapability = 'tool/dataprivacy:managedataregistry') { global $PAGE, $SITE; $context = context_system::instance(); require_login(); if (isguestuser()) { throw new \moodle_exception('noguest'); } // TODO Check that data privacy is enabled. require_capability($requiredcapability, $context); $PAGE->navigation->override_active_url($url); $PAGE->set_url($url); $PAGE->set_context($context); $PAGE->set_pagelayout('admin'); $PAGE->set_title($title); $PAGE->set_heading($SITE->fullname); $PAGE->set_secondary_active_tab('users'); $PAGE->set_primary_active_tab('siteadminnode'); // If necessary, override the settings navigation to add this page into the breadcrumb navigation. if ($attachtoparentnode) { if ($siteadmin = $PAGE->settingsnav->find('root', \navigation_node::TYPE_SITE_ADMIN)) { $PAGE->navbar->add($siteadmin->get_content(), $siteadmin->action()); } if ($dataprivacy = $PAGE->settingsnav->find('privacy', \navigation_node::TYPE_SETTING)) { $PAGE->navbar->add($dataprivacy->get_content(), $dataprivacy->action()); } if ($dataregistry = $PAGE->settingsnav->find($attachtoparentnode, \navigation_node::TYPE_SETTING)) { $PAGE->navbar->add($dataregistry->get_content(), $dataregistry->action()); } $PAGE->navbar->add($title, $url); } } } expiry_info.php 0000644 00000014461 15152024213 0007611 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Expiry Data. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use core_privacy\manager; defined('MOODLE_INTERNAL') || die(); /** * Expiry Data. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expiry_info { /** @var bool Whether this context is fully expired */ protected $fullyexpired = false; /** @var bool Whether the default expiry value of this purpose has been reached */ protected $defaultexpiryreached = false; /** @var bool Whether the default purpose is protected */ protected $defaultprotected = false; /** @var int[] List of expires roles */ protected $expired = []; /** @var int[] List of unexpires roles */ protected $unexpired = []; /** @var int[] List of unexpired roles which are also protected */ protected $protectedroles = []; /** * Constructor for the expiry_info class. * * @param bool $default Whether the default expiry period for this context has been reached. * @param bool $defaultprotected Whether the default expiry is protected. * @param int[] $expired A list of roles in this context which have explicitly expired. * @param int[] $unexpired A list of roles in this context which have not yet expired. * @param int[] $protectedroles A list of unexpired roles in this context which are protected. */ public function __construct(bool $default, bool $defaultprotected, array $expired, array $unexpired, array $protectedroles) { $this->defaultexpiryreached = $default; $this->defaultprotected = $defaultprotected; $this->expired = $expired; $this->unexpired = $unexpired; $this->protectedroles = $protectedroles; } /** * Whether this context has 'fully' expired. * That is to say that the default retention period has been reached, and that there are no unexpired roles. * * @return bool */ public function is_fully_expired() : bool { return $this->defaultexpiryreached && empty($this->unexpired); } /** * Whether any part of this context has expired. * * @return bool */ public function is_any_expired() : bool { if ($this->is_fully_expired()) { return true; } if (!empty($this->get_expired_roles())) { return true; } if ($this->is_default_expired()) { return true; } return false; } /** * Get the list of explicitly expired role IDs. * Note: This does not list roles which have been expired via the default retention policy being reached. * * @return int[] */ public function get_expired_roles() : array { if ($this->is_default_expired()) { return []; } return $this->expired; } /** * Check whether the specified role is explicitly expired. * Note: This does not list roles which have been expired via the default retention policy being reached. * * @param int $roleid * @return bool */ public function is_role_expired(int $roleid) : bool { return false !== array_search($roleid, $this->expired); } /** * Whether the default retention policy has been reached. * * @return bool */ public function is_default_expired() : bool { return $this->defaultexpiryreached; } /** * Whether the default purpose is protected. * * @return bool */ public function is_default_protected() : bool { return $this->defaultprotected; } /** * Get the list of unexpired role IDs. * * @return int[] */ public function get_unexpired_roles() : array { return $this->unexpired; } /** * Get the list of unexpired protected roles. * * @return int[] */ public function get_unexpired_protected_roles() : array { return array_keys(array_filter($this->protectedroles)); } /** * Get a list of all overridden roles which are unprotected. * @return int[] */ public function get_unprotected_overridden_roles() : array { $allroles = array_merge($this->expired, $this->unexpired); return array_diff($allroles, $this->protectedroles); } /** * Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage. * * It is not possible to delete any part of a context that is not deleted by a parent. * So if a course's retention policy has been reached, then only parts where the children have also expired can be * deleted. * * @param expiry_info $child The child record to merge with. * @return $this */ public function merge_with_child(expiry_info $child) : expiry_info { if ($child->is_fully_expired()) { return $this; } // If the child is not fully expired, then none of the parents can be either. $this->fullyexpired = false; // Remove any role in this node which is not expired in the child. foreach ($this->expired as $key => $roleid) { if (!$child->is_role_expired($roleid)) { unset($this->expired[$key]); } } array_merge($this->unexpired, $child->get_unexpired_roles()); if (!$child->is_default_expired()) { $this->defaultexpiryreached = false; } return $this; } } purpose_override.php 0000644 00000011131 15152024213 0010641 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing data purpose overrides from the DB. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Class for loading/storing data purpose overrides from the DB. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose_override extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_purposerole'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'purposeid' => array( 'type' => PARAM_INT, 'description' => 'The purpose that that this override relates to', ), 'roleid' => array( 'type' => PARAM_INT, 'description' => 'The role that that this override relates to', ), 'lawfulbases' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.', 'null' => NULL_ALLOWED, 'default' => null, ), 'sensitivedatareasons' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive', 'null' => NULL_ALLOWED, 'default' => null, ), 'retentionperiod' => array( 'type' => PARAM_ALPHANUM, 'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).', 'default' => '', ), 'protected' => array( 'type' => PARAM_INT, 'description' => 'Data retention with higher precedent over user\'s request to be forgotten.', 'default' => '0', ), ); } /** * Get all role overrides for the purpose. * * @param purpose $purpose * @return array */ public static function get_overrides_for_purpose(purpose $purpose) : array { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $overrides = []; $alldata = $cache->get($purpose->get('id')); if (false === $alldata) { $tocache = []; foreach (self::get_records(['purposeid' => $purpose->get('id')]) as $override) { $tocache[] = $override->to_record(); $overrides[$override->get('roleid')] = $override; } $cache->set($purpose->get('id'), $tocache); } else { foreach ($alldata as $data) { $override = new self(0, $data); $overrides[$override->get('roleid')] = $override; } } return $overrides; } /** * Adds the new record to the cache. * * @return null */ protected function after_create() { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $cache->delete($this->get('purposeid')); } /** * Updates the cache record. * * @param bool $result * @return null */ protected function after_update($result) { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $cache->delete($this->get('purposeid')); } /** * Removes unnecessary stuff from db. * * @return null */ protected function before_delete() { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $cache->delete($this->get('purposeid')); } } form/context_instance.php 0000644 00000020547 15152024213 0011573 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the form add/update context instance data. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use tool_dataprivacy\api; use tool_dataprivacy\data_registry; use tool_dataprivacy\purpose; /** * Context instance data form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context_instance extends \core\form\persistent { /** * @var The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\context_instance'; /** * Define the form - called by parent constructor */ public function definition() { $this->_form->setDisableShortforms(); $this->_form->addElement('header', 'contextname', $this->_customdata['contextname']); $subjectscope = implode(', ', $this->_customdata['subjectscope']); if (empty($subjectscope)) { $subjectscope = get_string('noassignedroles', 'tool_dataprivacy'); } $this->_form->addElement('static', 'subjectscope', get_string('subjectscope', 'tool_dataprivacy'), $subjectscope); $this->_form->addHelpButton('subjectscope', 'subjectscope', 'tool_dataprivacy'); $this->add_purpose_category($this->_customdata['context']->contextlevel); $this->_form->addElement('hidden', 'contextid'); $this->_form->setType('contextid', PARAM_INT); parent::add_action_buttons(false, get_string('savechanges')); } /** * Adds purpose and category selectors. * * @param int $contextlevel Apply this context level defaults. False for no defaults. * @return null */ protected function add_purpose_category($contextlevel = false) { $mform = $this->_form; $addcategorytext = $this->get_add_element_content(get_string('addcategory', 'tool_dataprivacy')); $categoryselect = $mform->createElement('select', 'categoryid', null, $this->_customdata['categories']); $addcategory = $mform->createElement('button', 'addcategory', $addcategorytext, ['data-add-element' => 'category']); $mform->addElement('group', 'categorygroup', get_string('category', 'tool_dataprivacy'), [$categoryselect, $addcategory], null, false); $mform->addHelpButton('categorygroup', 'category', 'tool_dataprivacy'); $mform->setType('categoryid', PARAM_INT); $mform->setDefault('categoryid', 0); $addpurposetext = $this->get_add_element_content(get_string('addpurpose', 'tool_dataprivacy')); $purposeselect = $mform->createElement('select', 'purposeid', null, $this->_customdata['purposes']); $addpurpose = $mform->createElement('button', 'addpurpose', $addpurposetext, ['data-add-element' => 'purpose']); $mform->addElement('group', 'purposegroup', get_string('purpose', 'tool_dataprivacy'), [$purposeselect, $addpurpose], null, false); $mform->addHelpButton('purposegroup', 'purpose', 'tool_dataprivacy'); $mform->setType('purposeid', PARAM_INT); $mform->setDefault('purposeid', 0); if (!empty($this->_customdata['currentretentionperiod'])) { $mform->addElement('static', 'retention_current', get_string('retentionperiod', 'tool_dataprivacy'), $this->_customdata['currentretentionperiod']); $mform->addHelpButton('retention_current', 'retentionperiod', 'tool_dataprivacy'); } } /** * Returns the 'add' label. * * It depends on the theme in use. * * @param string $label * @return \renderable|string */ private function get_add_element_content($label) { global $PAGE, $OUTPUT; $bs4 = false; $theme = $PAGE->theme; if ($theme->name === 'boost') { $bs4 = true; } else { foreach ($theme->parents as $basetheme) { if ($basetheme === 'boost') { $bs4 = true; } } } if (!$bs4) { return $label; } return $OUTPUT->pix_icon('e/insert', $label); } /** * Returns the customdata array for the provided context instance. * * @param \context $context * @return array */ public static function get_context_instance_customdata(\context $context) { $persistent = \tool_dataprivacy\context_instance::get_record_by_contextid($context->id, false); if (!$persistent) { $persistent = new \tool_dataprivacy\context_instance(); $persistent->set('contextid', $context->id); } $purposes = []; foreach (api::get_purposes() as $purpose) { $purposes[$purpose->get('id')] = $purpose; } $purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options($purposes); $categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options(api::get_categories()); $customdata = [ 'context' => $context, 'subjectscope' => data_registry::get_subject_scope($context), 'contextname' => $context->get_context_name(), 'persistent' => $persistent, 'purposes' => $purposeoptions, 'categories' => $categoryoptions, ]; $effectivepurpose = api::get_effective_context_purpose($context); if ($effectivepurpose) { $customdata['currentretentionperiod'] = self::get_retention_display_text($effectivepurpose, $context->contextlevel, $context); $customdata['purposeretentionperiods'] = []; foreach (array_keys($purposeoptions) as $optionvalue) { if (isset($purposes[$optionvalue])) { $purpose = $purposes[$optionvalue]; } else { // Get the effective purpose if $optionvalue would be the selected value. $purpose = api::get_effective_context_purpose($context, $optionvalue); } $retentionperiod = self::get_retention_display_text( $purpose, $context->contextlevel, $context ); $customdata['purposeretentionperiods'][$optionvalue] = $retentionperiod; } } return $customdata; } /** * Returns the purpose display text. * * @param purpose $effectivepurpose * @param int $retentioncontextlevel * @param \context $context The context, just for displaying (filters) purposes. * @return string */ protected static function get_retention_display_text(purpose $effectivepurpose, $retentioncontextlevel, \context $context) { global $PAGE; $renderer = $PAGE->get_renderer('tool_dataprivacy'); $exporter = new \tool_dataprivacy\external\purpose_exporter($effectivepurpose, ['context' => $context]); $exportedpurpose = $exporter->export($renderer); switch ($retentioncontextlevel) { case CONTEXT_COURSE: case CONTEXT_MODULE: case CONTEXT_BLOCK: $str = get_string('effectiveretentionperiodcourse', 'tool_dataprivacy', $exportedpurpose->formattedretentionperiod); break; case CONTEXT_USER: $str = get_string('effectiveretentionperioduser', 'tool_dataprivacy', $exportedpurpose->formattedretentionperiod); break; default: $str = $exportedpurpose->formattedretentionperiod; } return $str; } } form/purpose.php 0000644 00000044141 15152024213 0007714 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the form add/update a data purpose. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use core\form\persistent; /** * Data purpose form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose extends persistent { /** * @var string The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\purpose'; /** * @var array The list of current overrides. */ protected $existingoverrides = []; /** * Define the form - called by parent constructor */ public function definition() { $mform = $this->_form; $mform->addElement('text', 'name', get_string('name'), 'maxlength="100"'); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', get_string('required'), 'required', null, 'server'); $mform->addRule('name', get_string('maximumchars', '', 100), 'maxlength', 100, 'server'); $mform->addElement('editor', 'description', get_string('description'), null, ['autosave' => false]); $mform->setType('description', PARAM_CLEANHTML); // Field for selecting lawful bases (from GDPR Article 6.1). $this->add_field($this->get_lawful_base_field()); $mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server'); // Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2). $this->add_field($this->get_sensitive_base_field()); $this->add_field($this->get_retention_period_fields()); $this->add_field($this->get_protected_field()); $this->add_override_fields(); if (!empty($this->_customdata['showbuttons'])) { if (!$this->get_persistent()->get('id')) { $savetext = get_string('add'); } else { $savetext = get_string('savechanges'); } $this->add_action_buttons(true, $savetext); } } /** * Add a fieldset to the current form. * * @param \stdClass $data */ protected function add_field(\stdClass $data) { foreach ($data->fields as $field) { $this->_form->addElement($field); } if (!empty($data->helps)) { foreach ($data->helps as $fieldname => $helpdata) { $help = array_merge([$fieldname], $helpdata); call_user_func_array([$this->_form, 'addHelpButton'], $help); } } if (!empty($data->types)) { foreach ($data->types as $fieldname => $type) { $this->_form->setType($fieldname, $type); } } if (!empty($data->rules)) { foreach ($data->rules as $fieldname => $ruledata) { $rule = array_merge([$fieldname], $ruledata); call_user_func_array([$this->_form, 'addRule'], $rule); } } if (!empty($data->defaults)) { foreach ($data->defaults as $fieldname => $default) { $this->_form($fieldname, $default); } } } /** * Handle addition of relevant repeated element fields for role overrides. */ protected function add_override_fields() { $purpose = $this->get_persistent(); if (empty($purpose->get('id'))) { // It is not possible to use repeated elements in a modal form yet. return; } $fields = [ $this->get_role_override_id('roleoverride_'), $this->get_role_field('roleoverride_'), $this->get_retention_period_fields('roleoverride_'), $this->get_protected_field('roleoverride_'), $this->get_lawful_base_field('roleoverride_'), $this->get_sensitive_base_field('roleoverride_'), ]; $options = [ 'type' => [], 'helpbutton' => [], ]; // Start by adding the title. $overrideelements = [ $this->_form->createElement('header', 'roleoverride', get_string('roleoverride', 'tool_dataprivacy')), $this->_form->createElement( 'static', 'roleoverrideoverview', '', get_string('roleoverrideoverview', 'tool_dataprivacy') ), ]; foreach ($fields as $fielddata) { foreach ($fielddata->fields as $field) { $overrideelements[] = $field; } if (!empty($fielddata->helps)) { foreach ($fielddata->helps as $name => $help) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['helpbutton'] = $help; } } if (!empty($fielddata->types)) { foreach ($fielddata->types as $name => $type) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['type'] = $type; } } if (!empty($fielddata->rules)) { foreach ($fielddata->rules as $name => $rule) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['rule'] = $rule; } } if (!empty($fielddata->defaults)) { foreach ($fielddata->defaults as $name => $default) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['default'] = $default; } } if (!empty($fielddata->advanceds)) { foreach ($fielddata->advanceds as $name => $advanced) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['advanced'] = $advanced; } } } $this->existingoverrides = $purpose->get_purpose_overrides(); $existingoverridecount = count($this->existingoverrides); $this->repeat_elements( $overrideelements, $existingoverridecount, $options, 'overrides', 'addoverride', 1, get_string('addroleoverride', 'tool_dataprivacy') ); } /** * Converts fields. * * @param \stdClass $data * @return \stdClass */ public function filter_data_for_persistent($data) { $data = parent::filter_data_for_persistent($data); $classname = static::$persistentclass; $properties = $classname::properties_definition(); $data = (object) array_filter((array) $data, function($value, $key) use ($properties) { return isset($properties[$key]); }, ARRAY_FILTER_USE_BOTH); return $data; } /** * Get the field for the role name. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_role_override_id(string $prefix = '') : \stdClass { $fieldname = "{$prefix}id"; $fielddata = (object) [ 'fields' => [], ]; $fielddata->fields[] = $this->_form->createElement('hidden', $fieldname); $fielddata->types[$fieldname] = PARAM_INT; return $fielddata; } /** * Get the field for the role name. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_role_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}roleid"; $fielddata = (object) [ 'fields' => [], 'helps' => [], ]; $roles = [ '' => get_string('none'), ]; foreach (role_get_names() as $roleid => $role) { $roles[$roleid] = $role->localname; } $fielddata->fields[] = $this->_form->createElement('select', $fieldname, get_string('role'), $roles, [ 'multiple' => false, ] ); $fielddata->helps[$fieldname] = ['role', 'tool_dataprivacy']; $fielddata->defaults[$fieldname] = null; return $fielddata; } /** * Get the mform field for lawful bases. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_lawful_base_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}lawfulbases"; $data = (object) [ 'fields' => [], ]; $bases = []; foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) { $key = 'gdpr_art_6_1_' . $article; $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy'); } $data->fields[] = $this->_form->createElement('autocomplete', $fieldname, get_string('lawfulbases', 'tool_dataprivacy'), $bases, [ 'multiple' => true, ] ); $data->helps = [ $fieldname => ['lawfulbases', 'tool_dataprivacy'], ]; $data->advanceds = [ $fieldname => true, ]; return $data; } /** * Get the mform field for sensitive bases. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_sensitive_base_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}sensitivedatareasons"; $data = (object) [ 'fields' => [], ]; $bases = []; foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) { $key = 'gdpr_art_9_2_' . $article; $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy'); } $data->fields[] = $this->_form->createElement( 'autocomplete', $fieldname, get_string('sensitivedatareasons', 'tool_dataprivacy'), $bases, [ 'multiple' => true, ] ); $data->helps = [ $fieldname => ['sensitivedatareasons', 'tool_dataprivacy'], ]; $data->advanceds = [ $fieldname => true, ]; return $data; } /** * Get the retention period fields. * * @param string $prefix The name of the main field, and prefix for the subfields. * @return \stdClass */ protected function get_retention_period_fields(string $prefix = '') : \stdClass { $prefix = "{$prefix}retentionperiod"; $data = (object) [ 'fields' => [], 'types' => [], ]; $number = $this->_form->createElement('text', "{$prefix}number", null, ['size' => 8]); $data->types["{$prefix}number"] = PARAM_INT; $unitoptions = [ 'Y' => get_string('years'), 'M' => strtolower(get_string('months')), 'D' => strtolower(get_string('days')) ]; $unit = $this->_form->createElement('select', "{$prefix}unit", '', $unitoptions); $data->fields[] = $this->_form->createElement( 'group', $prefix, get_string('retentionperiod', 'tool_dataprivacy'), [ 'number' => $number, 'unit' => $unit, ], null, false ); return $data; } /** * Get the mform field for the protected flag. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_protected_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}protected"; return (object) [ 'fields' => [ $this->_form->createElement( 'advcheckbox', $fieldname, get_string('protected', 'tool_dataprivacy'), get_string('protectedlabel', 'tool_dataprivacy') ), ], ]; } /** * Converts data to data suitable for storage. * * @param \stdClass $data * @return \stdClass */ protected static function convert_fields(\stdClass $data) { $data = parent::convert_fields($data); if (!empty($data->lawfulbases) && is_array($data->lawfulbases)) { $data->lawfulbases = implode(',', $data->lawfulbases); } if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) { $data->sensitivedatareasons = implode(',', $data->sensitivedatareasons); } else { // Nothing selected. Set default value of null. $data->sensitivedatareasons = null; } // A single value. $data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit; unset($data->retentionperiodnumber); unset($data->retentionperiodunit); return $data; } /** * Get the default data. * * @return \stdClass */ protected function get_default_data() { $data = parent::get_default_data(); return $this->convert_existing_data_to_values($data); } /** * Normalise any values stored in existing data. * * @param \stdClass $data * @return \stdClass */ protected function convert_existing_data_to_values(\stdClass $data) : \stdClass { $data->lawfulbases = explode(',', $data->lawfulbases); if (!empty($data->sensitivedatareasons)) { $data->sensitivedatareasons = explode(',', $data->sensitivedatareasons); } // Convert the single properties into number and unit. $strlen = strlen($data->retentionperiod); $data->retentionperiodnumber = substr($data->retentionperiod, 1, $strlen - 2); $data->retentionperiodunit = substr($data->retentionperiod, $strlen - 1); unset($data->retentionperiod); return $data; } /** * Fetch the role override data from the list of submitted data. * * @param \stdClass $data The complete set of processed data * @return \stdClass[] The list of overrides */ public function get_role_overrides_from_data(\stdClass $data) { $overrides = []; if (!empty($data->overrides)) { $searchkey = 'roleoverride_'; for ($i = 0; $i < $data->overrides; $i++) { $overridedata = (object) []; foreach ((array) $data as $fieldname => $value) { if (strpos($fieldname, $searchkey) !== 0) { continue; } $overridefieldname = substr($fieldname, strlen($searchkey)); $overridedata->$overridefieldname = $value[$i]; } if (empty($overridedata->roleid) || empty($overridedata->retentionperiodnumber)) { // Skip this one. // There is no value and it will be delete. continue; } $override = static::convert_fields($overridedata); $overrides[$i] = $override; } } return $overrides; } /** * Define extra validation mechanims. * * @param stdClass $data Data to validate. * @param array $files Array of files. * @param array $errors Currently reported errors. * @return array of additional errors, or overridden errors. */ protected function extra_validation($data, $files, array &$errors) { $overrides = $this->get_role_overrides_from_data($data); // Check role overrides to ensure that: // - roles are unique; and // - specifeid retention periods are numeric. $seenroleids = []; foreach ($overrides as $id => $override) { $override->purposeid = 0; $persistent = new \tool_dataprivacy\purpose_override($override->id, $override); if (isset($seenroleids[$persistent->get('roleid')])) { $errors["roleoverride_roleid[{$id}]"] = get_string('duplicaterole'); } $seenroleids[$persistent->get('roleid')] = true; $errors = array_merge($errors, $persistent->get_errors()); } return $errors; } /** * Load in existing data as form defaults. Usually new entry defaults are stored directly in * form definition (new entry form); this function is used to load in data where values * already exist and data is being edited (edit entry form). * * @param stdClass $data */ public function set_data($data) { $purpose = $this->get_persistent(); $count = 0; foreach ($this->existingoverrides as $override) { $overridedata = $this->convert_existing_data_to_values($override->to_record()); foreach ($overridedata as $key => $value) { $keyname = "roleoverride_{$key}[{$count}]"; $data->$keyname = $value; } $count++; } parent::set_data($data); } } form/category.php 0000644 00000004324 15152024213 0010033 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the form add/update a data category. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use core\form\persistent; /** * Data category form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category extends persistent { /** * @var The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\category'; /** * Define the form - called by parent constructor */ public function definition() { $mform = $this->_form; $mform->addElement('text', 'name', get_string('name'), 'maxlength="100"'); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', get_string('required'), 'required', null, 'server'); $mform->addRule('name', get_string('maximumchars', '', 100), 'maxlength', 100, 'server'); $mform->addElement('editor', 'description', get_string('description'), null, ['autosave' => false]); $mform->setType('description', PARAM_CLEANHTML); if (!empty($this->_customdata['showbuttons'])) { if (!$this->get_persistent()->get('id')) { $savetext = get_string('add'); } else { $savetext = get_string('savechanges'); } $this->add_action_buttons(true, $savetext); } } } form/contextlevel.php 0000644 00000010657 15152024213 0010740 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the form add/update context level data. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use core\form\persistent; use tool_dataprivacy\api; use tool_dataprivacy\data_registry; /** * Context level data form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contextlevel extends context_instance { /** * @var The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\contextlevel'; /** * Define the form - called by parent constructor */ public function definition() { $this->_form->setDisableShortforms(); $this->_form->addElement('header', 'contextlevelname', $this->_customdata['contextlevelname']); $this->add_purpose_category(); $this->_form->addElement('hidden', 'contextlevel'); $this->_form->setType('contextlevel', PARAM_INT); parent::add_action_buttons(false, get_string('savechanges')); } /** * Returns the customdata array for the provided context level. * * @param int $contextlevel * @return array */ public static function get_contextlevel_customdata($contextlevel) { $persistent = \tool_dataprivacy\contextlevel::get_record_by_contextlevel($contextlevel, false); if (!$persistent) { $persistent = new \tool_dataprivacy\contextlevel(); $persistent->set('contextlevel', $contextlevel); } $includeinherit = true; if ($contextlevel == CONTEXT_SYSTEM) { // Nothing to inherit from Site level. $includeinherit = false; } $includenotset = true; if ($contextlevel == CONTEXT_SYSTEM || $contextlevel == CONTEXT_USER) { // No 'not set' value for system and user because we do not have defaults for them. $includenotset = false; } $purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options( api::get_purposes(), $includenotset, $includeinherit); $categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options( api::get_categories(), $includenotset, $includeinherit); $customdata = [ 'contextlevel' => $contextlevel, 'contextlevelname' => get_string('contextlevelname' . $contextlevel, 'tool_dataprivacy'), 'persistent' => $persistent, 'purposes' => $purposeoptions, 'categories' => $categoryoptions, ]; $effectivepurpose = api::get_effective_contextlevel_purpose($contextlevel); if ($effectivepurpose) { $customdata['currentretentionperiod'] = self::get_retention_display_text($effectivepurpose, $contextlevel, \context_system::instance()); $customdata['purposeretentionperiods'] = []; foreach ($purposeoptions as $optionvalue => $unused) { // Get the effective purpose if $optionvalue would be the selected value. list($purposeid, $unused) = data_registry::get_effective_default_contextlevel_purpose_and_category($contextlevel, $optionvalue); $purpose = new \tool_dataprivacy\purpose($purposeid); $retentionperiod = self::get_retention_display_text( $purpose, $contextlevel, \context_system::instance() ); $customdata['purposeretentionperiods'][$optionvalue] = $retentionperiod; } } return $customdata; } } form/contactdpo.php 0000644 00000005541 15152024213 0010356 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace tool_dataprivacy\form; use context; use context_user; use moodle_exception; use moodle_url; use core_form\dynamic_form; use tool_dataprivacy\api; use tool_dataprivacy\external; /** * Contact DPO modal form * * @package tool_dataprivacy * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contactdpo extends dynamic_form { /** * Form definition */ protected function definition() { global $USER; $mform = $this->_form; $mform->addElement('static', 'replyto', get_string('replyto', 'tool_dataprivacy'), s($USER->email)); $mform->addElement('textarea', 'message', get_string('message', 'tool_dataprivacy'), 'cols="60" rows="8"'); $mform->setType('message', PARAM_TEXT); $mform->addRule('message', get_string('required'), 'required', null, 'client'); } /** * Return form context * * @return context */ protected function get_context_for_dynamic_submission(): context { global $USER; return context_user::instance($USER->id); } /** * Check if current user has access to this form, otherwise throw exception * * @throws moodle_exception */ protected function check_access_for_dynamic_submission(): void { if (!api::can_contact_dpo()) { throw new moodle_exception('errorcontactdpodisabled', 'tool_dataprivacy'); } } /** * Process the form submission, used if form was submitted via AJAX * * @return array */ public function process_dynamic_submission() { return external::contact_dpo($this->get_data()->message); } /** * Load in existing data as form defaults (not applicable) */ public function set_data_for_dynamic_submission(): void { return; } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { global $USER; return new moodle_url('/user/profile.php', ['id' => $USER->id]); } } purpose.php 0000644 00000013755 15152024213 0006760 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing data purposes from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Class for loading/storing data purposes from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_purpose'; /** Items under GDPR Article 6.1. */ const GDPR_ART_6_1_ITEMS = ['a', 'b', 'c', 'd', 'e', 'f']; /** Items under GDPR Article 9.2. */ const GDPR_ART_9_2_ITEMS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; /** * Extended constructor to fetch from the cache if available. * * @param int $id If set, this is the id of an existing record, used to load the data. * @param stdClass $record If set will be passed to {@link self::from_record()}. */ public function __construct($id = 0, stdClass $record = null) { global $CFG; if ($id) { $cache = \cache::make('tool_dataprivacy', 'purpose'); if ($data = $cache->get($id)) { // Replicate self::read. $this->from_record($data); // Validate the purpose record. $this->validate(); // Now replicate the parent constructor. if (!empty($record)) { $this->from_record($record); } if ($CFG->debugdeveloper) { $this->verify_protected_methods(); } return; } } parent::__construct($id, $record); } /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'name' => array( 'type' => PARAM_TEXT, 'description' => 'The purpose name.', ), 'description' => array( 'type' => PARAM_RAW, 'description' => 'The purpose description.', 'null' => NULL_ALLOWED, 'default' => '', ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML ), 'lawfulbases' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.', ), 'sensitivedatareasons' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive', 'null' => NULL_ALLOWED, 'default' => '' ), 'retentionperiod' => array( 'type' => PARAM_ALPHANUM, 'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).', 'default' => '', ), 'protected' => array( 'type' => PARAM_INT, 'description' => 'Data retention with higher precedent over user\'s request to be forgotten.', 'default' => '0', ), ); } /** * Adds the new record to the cache. * * @return null */ protected function after_create() { $cache = \cache::make('tool_dataprivacy', 'purpose'); $cache->set($this->get('id'), $this->to_record()); } /** * Updates the cache record. * * @param bool $result * @return null */ protected function after_update($result) { $cache = \cache::make('tool_dataprivacy', 'purpose'); $cache->set($this->get('id'), $this->to_record()); } /** * Removes unnecessary stuff from db. * * @return null */ protected function before_delete() { $cache = \cache::make('tool_dataprivacy', 'purpose'); $cache->delete($this->get('id')); } /** * Is this purpose used?. * * @return null */ public function is_used() { if (\tool_dataprivacy\contextlevel::is_purpose_used($this->get('id')) || \tool_dataprivacy\context_instance::is_purpose_used($this->get('id'))) { return true; } $pluginconfig = get_config('tool_dataprivacy'); $levels = \context_helper::get_all_levels(); foreach ($levels as $level => $classname) { list($purposevar, $unused) = \tool_dataprivacy\data_registry::var_names_from_context($classname); if (!empty($pluginconfig->{$purposevar}) && $pluginconfig->{$purposevar} == $this->get('id')) { return true; } } return false; } /** * Get a list of the role purpose overrides for this purpose. * * @return array */ public function get_purpose_overrides() : array { return purpose_override::get_overrides_for_purpose($this); } } event/user_deleted_observer.php 0000644 00000004574 15152024213 0012756 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Event observers supported by this module. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\event; use \tool_dataprivacy\api; use \tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); /** * Event observers supported by this module. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_deleted_observer { /** * Create user data deletion request when the user is deleted. * * @param \core\event\user_deleted $event */ public static function create_delete_data_request(\core\event\user_deleted $event) { // Automatic creation of deletion requests must be enabled. if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) { $requesttypes = [api::DATAREQUEST_TYPE_DELETE]; $requeststatuses = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DELETED]; $hasongoingdeleterequests = api::has_ongoing_request($event->objectid, $requesttypes[0]); $hascompleteddeleterequest = (api::get_data_requests_count($event->objectid, $requeststatuses, $requesttypes) > 0) ? true : false; if (!$hasongoingdeleterequests && !$hascompleteddeleterequest) { api::create_data_request($event->objectid, $requesttypes[0], get_string('datarequestcreatedupondelete', 'tool_dataprivacy'), data_request::DATAREQUEST_CREATION_AUTO); } } } } category.php 0000644 00000005546 15152024213 0007077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing data categories from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Class for loading/storing data categories from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_category'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'name' => array( 'type' => PARAM_TEXT, 'description' => 'The category name.', ), 'description' => array( 'type' => PARAM_RAW, 'description' => 'The category description.', 'null' => NULL_ALLOWED, 'default' => '', ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML ), ); } /** * Is this category used?. * * @return null */ public function is_used() { if (\tool_dataprivacy\contextlevel::is_category_used($this->get('id')) || \tool_dataprivacy\context_instance::is_category_used($this->get('id'))) { return true; } $pluginconfig = get_config('tool_dataprivacy'); $levels = \context_helper::get_all_levels(); foreach ($levels as $level => $classname) { list($unused, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); if (!empty($pluginconfig->{$categoryvar}) && $pluginconfig->{$categoryvar} == $this->get('id')) { return true; } } return false; } } task/expired_retention_period.php 0000644 00000003620 15152024213 0013304 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Scheduled task to flag contexts as expired. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use coding_exception; use core\task\scheduled_task; use tool_dataprivacy\api; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to flag contexts as expired. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_retention_period extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('expiredretentionperiodtask', 'tool_dataprivacy'); } /** * Run the task to flag context instances as expired. */ public function execute() { $manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace()); list($courses, $users) = $manager->flag_expired_contexts(); mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired"); } } task/delete_existing_deleted_users.php 0000644 00000005772 15152024213 0014310 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Scheduled task to create delete data request for pre-existing deleted users. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use core\task\scheduled_task; use tool_dataprivacy\api; use tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to create delete data request for pre-existing deleted users. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_existing_deleted_users extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('deleteexistingdeleteduserstask', 'tool_dataprivacy'); } /** * Run the task to delete expired data request files and update request statuses. * */ public function execute() { global $DB; // Automatic creation of deletion requests must be enabled. if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) { // Select all deleted users that do not have any delete data requests created for them. $sql = "SELECT DISTINCT(u.id) FROM {user} u LEFT JOIN {tool_dataprivacy_request} r ON u.id = r.userid WHERE u.deleted = ? AND (r.id IS NULL OR r.type != ?)"; $params = [ 1, api::DATAREQUEST_TYPE_DELETE ]; $deletedusers = $DB->get_records_sql($sql, $params); $createdrequests = 0; foreach ($deletedusers as $user) { api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE, get_string('datarequestcreatedfromscheduledtask', 'tool_dataprivacy'), data_request::DATAREQUEST_CREATION_AUTO); $createdrequests++; } if ($createdrequests > 0) { mtrace($createdrequests . ' delete data request(s) created for existing deleted users'); } } } } task/delete_expired_requests.php 0000644 00000004106 15152024213 0013130 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Scheduled task to delete files and update statuses of expired data requests. * * @package tool_dataprivacy * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use coding_exception; use core\task\scheduled_task; use tool_dataprivacy\api; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to delete files and update request statuses once they expire. * * @package tool_dataprivacy * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_expired_requests extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('deleteexpireddatarequeststask', 'tool_dataprivacy'); } /** * Run the task to delete expired data request files and update request statuses. * */ public function execute() { $expiredrequests = \tool_dataprivacy\data_request::get_expired_requests(); $deletecount = count($expiredrequests); if ($deletecount > 0) { \tool_dataprivacy\data_request::expire($expiredrequests); mtrace($deletecount . ' expired completed data requests have been deleted'); } } } task/process_data_request_task.php 0000644 00000031706 15152024213 0013462 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Adhoc task that processes an approved data request and prepares/deletes the user's data. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use action_link; use coding_exception; use context_system; use core\message\message; use core\task\adhoc_task; use core_user; use moodle_exception; use moodle_url; use tool_dataprivacy\api; use tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); /** * Class that processes an approved data request and prepares/deletes the user's data. * * Custom data accepted: * - requestid -> The ID of the data request to be processed. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_data_request_task extends adhoc_task { /** * Run the task to initiate the data request process. * * @throws coding_exception * @throws moodle_exception */ public function execute() { global $CFG, $PAGE, $SITE; require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php"); if (!isset($this->get_custom_data()->requestid)) { throw new coding_exception('The custom data \'requestid\' is required.'); } $requestid = $this->get_custom_data()->requestid; $requestpersistent = new data_request($requestid); $request = $requestpersistent->to_record(); // Check if this request still needs to be processed. e.g. The user might have cancelled it before this task has run. $status = $requestpersistent->get('status'); if (!api::is_active($status)) { mtrace("Request {$requestid} with status {$status} doesn't need to be processed. Skipping..."); return; } if (!\tool_dataprivacy\data_registry::defaults_set()) { // Warn if no site purpose is defined. mtrace('Warning: No purpose is defined at the system level. Deletion will delete all.'); } // Grab the manager. // We set an observer against it to handle failures. $manager = new \core_privacy\manager(); $manager->set_observer(new \tool_dataprivacy\manager_observer()); // Get the user details now. We might not be able to retrieve it later if it's a deletion processing. $foruser = core_user::get_user($request->userid); // Update the status of this request as pre-processing. mtrace('Pre-processing request...'); api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING); $contextlistcollection = $manager->get_contexts_for_userid($requestpersistent->get('userid')); mtrace('Fetching approved contextlists from collection'); $approvedclcollection = api::get_approved_contextlist_collection_for_collection( $contextlistcollection, $foruser, $request->type); mtrace('Processing request...'); $completestatus = api::DATAREQUEST_STATUS_COMPLETE; $deleteuser = false; if ($request->type == api::DATAREQUEST_TYPE_EXPORT) { // Get the user context. $usercontext = \context_user::instance($foruser->id, IGNORE_MISSING); if (!$usercontext) { mtrace("Request {$requestid} cannot be processed due to a missing user context instance for the user with ID {$foruser->id}. Skipping..."); return; } // Export the data. $exportedcontent = $manager->export_user_data($approvedclcollection); $fs = get_file_storage(); $filerecord = new \stdClass; $filerecord->component = 'tool_dataprivacy'; $filerecord->contextid = $usercontext->id; $filerecord->userid = $foruser->id; $filerecord->filearea = 'export'; $filerecord->filename = 'export.zip'; $filerecord->filepath = '/'; $filerecord->itemid = $requestid; $filerecord->license = $CFG->sitedefaultlicense; $filerecord->author = fullname($foruser); // Save somewhere. $thing = $fs->create_file_from_pathname($filerecord, $exportedcontent); $completestatus = api::DATAREQUEST_STATUS_DOWNLOAD_READY; } else if ($request->type == api::DATAREQUEST_TYPE_DELETE) { // Delete the data for users other than the primary admin, which is rejected. if (is_primary_admin($foruser->id)) { $completestatus = api::DATAREQUEST_STATUS_REJECTED; } else { $manager = new \core_privacy\manager(); $manager->set_observer(new \tool_dataprivacy\manager_observer()); $manager->delete_data_for_user($approvedclcollection); $completestatus = api::DATAREQUEST_STATUS_DELETED; $deleteuser = !$foruser->deleted; } } // When the preparation of the metadata finishes, update the request status to awaiting approval. api::update_request_status($requestid, $completestatus); mtrace('The processing of the user data request has been completed...'); // Create message to notify the user regarding the processing results. $message = new message(); $message->courseid = $SITE->id; $message->component = 'tool_dataprivacy'; $message->name = 'datarequestprocessingresults'; if (empty($request->dpo)) { // Use the no-reply user as the sender if the privacy officer is not set. This is the case for automatically // approved requests. $fromuser = core_user::get_noreply_user(); } else { $fromuser = core_user::get_user($request->dpo); $message->replyto = $fromuser->email; $message->replytoname = fullname($fromuser); } $message->userfrom = $fromuser; $typetext = null; // Prepare the context data for the email message body. $messagetextdata = [ 'username' => fullname($foruser) ]; $output = $PAGE->get_renderer('tool_dataprivacy'); $emailonly = false; $notifyuser = true; switch ($request->type) { case api::DATAREQUEST_TYPE_EXPORT: // Check if the user is allowed to download their own export. (This is for // institutions which centrally co-ordinate subject access request across many // systems, not just one Moodle instance, so we don't want every instance emailing // the user.) if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) { $notifyuser = false; } $typetext = get_string('requesttypeexport', 'tool_dataprivacy'); // We want to notify the user in Moodle about the processing results. $message->notification = 1; $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php'); $message->contexturl = $datarequestsurl; $message->contexturlname = get_string('datarequests', 'tool_dataprivacy'); // Message to the recipient. $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy', format_string($SITE->fullname, true, ['context' => context_system::instance()])); // Prepare download link. $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $thing->get_itemid(), $thing->get_filepath(), $thing->get_filename(), true); $downloadlink = new action_link($downloadurl, get_string('download', 'tool_dataprivacy')); $messagetextdata['downloadlink'] = $downloadlink->export_for_template($output); break; case api::DATAREQUEST_TYPE_DELETE: $typetext = get_string('requesttypedelete', 'tool_dataprivacy'); // No point notifying a deleted user in Moodle. $message->notification = 0; // Message to the recipient. $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy', format_string($SITE->fullname, true, ['context' => context_system::instance()])); // Message will be sent to the deleted user via email only. $emailonly = true; break; default: throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); } $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext); $message->subject = $subject; $message->fullmessageformat = FORMAT_HTML; $message->userto = $foruser; // Render message email body. $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata); $message->fullmessage = html_to_text($messagehtml); $message->fullmessagehtml = $messagehtml; // Send message to the user involved. if ($notifyuser) { $messagesent = false; if ($emailonly) { // Do not sent an email if the user has been deleted. The user email has been previously deleted. if (!$foruser->deleted) { $messagesent = email_to_user($foruser, $fromuser, $subject, $message->fullmessage, $messagehtml); } } else { $messagesent = message_send($message); } if ($messagesent) { mtrace('Message sent to user: ' . $messagetextdata['username']); } } // Send to requester as well in some circumstances. if ($foruser->id != $request->requestedby) { $sendtorequester = false; switch ($request->type) { case api::DATAREQUEST_TYPE_EXPORT: // Send to the requester as well if they can download it, unless they are the // DPO. If we didn't notify the user themselves (because they can't download) // then send to requester even if it is the DPO, as in that case the requester // needs to take some action. if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) { $sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby); } break; case api::DATAREQUEST_TYPE_DELETE: // Send to the requester if they are not the DPO and if they are allowed to // create data requests for the user (e.g. Parent). $sendtorequester = !api::is_site_dpo($request->requestedby) && api::can_create_data_request_for_user($request->userid, $request->requestedby); break; default: throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); } // Ensure the requester has the capability to make data requests for this user. if ($sendtorequester) { $requestedby = core_user::get_user($request->requestedby); $message->userto = $requestedby; $messagetextdata['username'] = fullname($requestedby); // Render message email body. $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata); $message->fullmessage = html_to_text($messagehtml); $message->fullmessagehtml = $messagehtml; // Send message. if ($emailonly) { email_to_user($requestedby, $fromuser, $subject, $message->fullmessage, $messagehtml); } else { message_send($message); } mtrace('Message sent to requester: ' . $messagetextdata['username']); } } if ($deleteuser) { // Delete the user. delete_user($foruser); } } } task/delete_expired_contexts.php 0000644 00000004023 15152024213 0013122 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Scheduled task to delete expired context instances once they are approved for deletion. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use coding_exception; use core\task\scheduled_task; use tool_dataprivacy\api; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to delete expired context instances once they are approved for deletion. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_expired_contexts extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('deleteexpiredcontextstask', 'tool_dataprivacy'); } /** * Run the task to delete context instances based on their retention periods. */ public function execute() { $manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace()); list($courses, $users) = $manager->process_approved_deletions(); mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired"); } } data_registry.php 0000644 00000034147 15152024213 0010122 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Data registry business logic methods. Mostly internal stuff. * * All methods should be considered part of the internal tool_dataprivacy API * unless something different is specified. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use coding_exception; use core\persistent; defined('MOODLE_INTERNAL') || die(); /** * Data registry business logic methods. Mostly internal stuff. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_registry { /** * Returns purpose and category var names from a context class name * * @param string $classname The context level's class. * @param string $pluginname The name of the plugin associated with the context level. * @return string[] */ public static function var_names_from_context($classname, $pluginname = '') { $pluginname = trim($pluginname ?? ''); if (!empty($pluginname)) { $categoryvar = $classname . '_' . $pluginname . '_category'; $purposevar = $classname . '_' . $pluginname . '_purpose'; } else { $categoryvar = $classname . '_category'; $purposevar = $classname . '_purpose'; } return [ $purposevar, $categoryvar ]; } /** * Returns the default purpose id and category id for the provided context level. * * The caller code is responsible of checking that $contextlevel is an integer. * * @param int $contextlevel The context level. * @param string $pluginname The name of the plugin associated with the context level. * @return int[]|false[] */ public static function get_defaults($contextlevel, $pluginname = '') { $classname = \context_helper::get_class_for_level($contextlevel); list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname); $purposeid = get_config('tool_dataprivacy', $purposevar); $categoryid = get_config('tool_dataprivacy', $categoryvar); if (!empty($pluginname)) { list($purposevar, $categoryvar) = self::var_names_from_context($classname); // If the plugin-level doesn't have a default purpose set, try the context level. if ($purposeid == false) { $purposeid = get_config('tool_dataprivacy', $purposevar); } // If the plugin-level doesn't have a default category set, try the context level. if ($categoryid == false) { $categoryid = get_config('tool_dataprivacy', $categoryvar); } } if (empty($purposeid)) { $purposeid = context_instance::NOTSET; } if (empty($categoryid)) { $categoryid = context_instance::NOTSET; } return [$purposeid, $categoryid]; } /** * Are data registry defaults set? * * At least the system defaults need to be set. * * @return bool */ public static function defaults_set() { list($purposeid, $categoryid) = self::get_defaults(CONTEXT_SYSTEM); if (empty($purposeid) || empty($categoryid)) { return false; } return true; } /** * Returns all site categories that are visible to the current user. * * @return \core_course_category[] */ public static function get_site_categories() { global $DB; if (method_exists('\core_course_category', 'get_all')) { $categories = \core_course_category::get_all(['returnhidden' => true]); } else { // Fallback (to be removed once this gets integrated into master). $ids = $DB->get_fieldset_select('course_categories', 'id', ''); $categories = \core_course_category::get_many($ids); } foreach ($categories as $key => $category) { if (!$category->is_uservisible()) { unset($categories[$key]); } } return $categories; } /** * Returns the roles assigned to the provided level. * * Important to note that it returns course-level assigned roles * if the provided context level is below course. * * @param \context $context * @return array */ public static function get_subject_scope(\context $context) { if ($contextcourse = $context->get_course_context(false)) { // Below course level we look at module or block level roles + course-assigned roles. $courseroles = get_roles_used_in_context($contextcourse, false); $roles = $courseroles + get_roles_used_in_context($context, false); } else { // We list category + system for others (we don't work with user instances so no need to work about them). $roles = get_roles_used_in_context($context); } return array_map(function($role) { if ($role->name) { return $role->name; } else { return $role->shortname; } }, $roles); } /** * Returns the effective value given a context instance * * @param \context $context * @param string $element 'category' or 'purpose' * @param int|false $forcedvalue Use this value as if this was this context instance value. * @return persistent|false It return a 'purpose' instance or a 'category' instance, depending on $element */ public static function get_effective_context_value(\context $context, $element, $forcedvalue = false) { global $DB; if ($element !== 'purpose' && $element !== 'category') { throw new coding_exception('Only \'purpose\' and \'category\' are supported.'); } $fieldname = $element . 'id'; if (!empty($forcedvalue) && ($forcedvalue == context_instance::INHERIT)) { // Do not include the current context when calculating the value. // This has the effect that an inheritted value is calculated. $parentcontextids = $context->get_parent_context_ids(false); } else if (!empty($forcedvalue) && ($forcedvalue != context_instance::NOTSET)) { return self::get_element_instance($element, $forcedvalue); } else { // Fetch all parent contexts, including self. $parentcontextids = $context->get_parent_context_ids(true); } list($insql, $inparams) = $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED); $inparams['contextmodule'] = CONTEXT_MODULE; if ('purpose' === $element) { $elementjoin = 'LEFT JOIN {tool_dataprivacy_purpose} ele ON ctxins.purposeid = ele.id'; $elementfields = purpose::get_sql_fields('ele', 'ele'); } else { $elementjoin = 'LEFT JOIN {tool_dataprivacy_category} ele ON ctxins.categoryid = ele.id'; $elementfields = category::get_sql_fields('ele', 'ele'); } $contextfields = \context_helper::get_preload_record_columns_sql('ctx'); $fields = implode(', ', ['ctx.id', 'm.name AS modname', $contextfields, $elementfields]); $sql = "SELECT $fields FROM {context} ctx LEFT JOIN {tool_dataprivacy_ctxinstance} ctxins ON ctx.id = ctxins.contextid LEFT JOIN {course_modules} cm ON ctx.contextlevel = :contextmodule AND ctx.instanceid = cm.id LEFT JOIN {modules} m ON m.id = cm.module {$elementjoin} WHERE ctx.id {$insql} ORDER BY ctx.path DESC"; $contextinstances = $DB->get_records_sql($sql, $inparams); // Check whether this context is a user context, or a child of a user context. // All children of a User context share the same context and cannot be set individually. foreach ($contextinstances as $record) { \context_helper::preload_from_record($record); $parent = \context::instance_by_id($record->id, false); if ($parent->contextlevel == CONTEXT_USER) { // Use the context level value for the user. return self::get_effective_contextlevel_value(CONTEXT_USER, $element); } } foreach ($contextinstances as $record) { $parent = \context::instance_by_id($record->id, false); $checkcontextlevel = false; if (empty($record->eleid)) { $checkcontextlevel = true; } if (!empty($forcedvalue) && context_instance::NOTSET == $forcedvalue) { $checkcontextlevel = true; } if ($checkcontextlevel) { // Check for a value at the contextlevel $forplugin = empty($record->modname) ? '' : $record->modname; list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category( $parent->contextlevel, false, false, $forplugin); $instancevalue = $$fieldname; if (context_instance::NOTSET != $instancevalue && context_instance::INHERIT != $instancevalue) { // There is an actual value. Return it. return self::get_element_instance($element, $instancevalue); } } else { $elementclass = "\\tool_dataprivacy\\{$element}"; $instance = new $elementclass(null, $elementclass::extract_record($record, 'ele')); $instance->validate(); return $instance; } } throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.'); } /** * Returns the effective value for a context level. * * Note that this is different from the effective default context level * (see get_effective_default_contextlevel_purpose_and_category) as this is returning * the value set in the data registry, not in the defaults page. * * @param int $contextlevel * @param string $element 'category' or 'purpose' * @return \tool_dataprivacy\purpose|false */ public static function get_effective_contextlevel_value($contextlevel, $element) { if ($element !== 'purpose' && $element !== 'category') { throw new coding_exception('Only \'purpose\' and \'category\' are supported.'); } $fieldname = $element . 'id'; if ($contextlevel != CONTEXT_SYSTEM && $contextlevel != CONTEXT_USER) { throw new \coding_exception('Only context_system and context_user values can be retrieved, no other context levels ' . 'have a purpose or a category.'); } list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel); // Note: The $$fieldname points to either $purposeid, or $categoryid. if (context_instance::NOTSET != $$fieldname && context_instance::INHERIT != $$fieldname) { // There is a specific value set. return self::get_element_instance($element, $$fieldname); } throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.'); } /** * Returns the effective default purpose and category for a context level. * * @param int $contextlevel * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose. * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category. * @param string $component The name of the component to check. * @return int[] */ public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false, $forcedcategoryvalue = false, $component = '') { // Get the defaults for this context level. list($purposeid, $categoryid) = self::get_defaults($contextlevel, $component); // Honour forced values. if ($forcedpurposevalue) { $purposeid = $forcedpurposevalue; } if ($forcedcategoryvalue) { $categoryid = $forcedcategoryvalue; } if ($contextlevel == CONTEXT_USER) { // Only user context levels inherit from a parent context level. list($parentpurposeid, $parentcategoryid) = self::get_defaults(CONTEXT_SYSTEM); if (context_instance::INHERIT == $purposeid || context_instance::NOTSET == $purposeid) { $purposeid = (int)$parentpurposeid; } if (context_instance::INHERIT == $categoryid || context_instance::NOTSET == $categoryid) { $categoryid = $parentcategoryid; } } return [$purposeid, $categoryid]; } /** * Returns an instance of the provided element. * * @throws \coding_exception * @param string $element The element name 'purpose' or 'category' * @param int $id The element id * @return \core\persistent */ private static function get_element_instance($element, $id) { if ($element !== 'purpose' && $element !== 'category') { throw new coding_exception('No other elements than purpose and category are allowed'); } $classname = '\tool_dataprivacy\\' . $element; return new $classname($id); } } contextlevel.php 0000644 00000007731 15152024213 0007774 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing context level data from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * Class for loading/storing context level data from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contextlevel extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_ctxlevel'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'contextlevel' => array( 'type' => PARAM_INT, 'description' => 'The context level.', ), 'purposeid' => array( 'type' => PARAM_INT, 'description' => 'The purpose id.', ), 'categoryid' => array( 'type' => PARAM_INT, 'description' => 'The category id.', ), ); } /** * Returns an instance by contextlevel. * * @param mixed $contextlevel * @param mixed $exception * @return null */ public static function get_record_by_contextlevel($contextlevel, $exception = true) { global $DB; $cache = \cache::make('tool_dataprivacy', 'contextlevel'); if ($data = $cache->get($contextlevel)) { return new static(0, $data); } if (!$record = $DB->get_record(self::TABLE, array('contextlevel' => $contextlevel))) { if (!$exception) { return false; } else { throw new \dml_missing_record_exception(self::TABLE); } } return new static(0, $record); } /** * Is the provided purpose used by any contextlevel? * * @param int $purposeid * @return bool */ public static function is_purpose_used($purposeid) { global $DB; return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid)); } /** * Is the provided category used by any contextlevel? * * @param int $categoryid * @return bool */ public static function is_category_used($categoryid) { global $DB; return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid)); } /** * Adds the new record to the cache. * * @return null */ protected function after_create() { $cache = \cache::make('tool_dataprivacy', 'contextlevel'); $cache->set($this->get('contextlevel'), $this->to_record()); } /** * Updates the cache record. * * @param bool $result * @return null */ protected function after_update($result) { $cache = \cache::make('tool_dataprivacy', 'contextlevel'); $cache->set($this->get('contextlevel'), $this->to_record()); } /** * Removes unnecessary stuff from db. * * @return null */ protected function before_delete() { $cache = \cache::make('tool_dataprivacy', 'contextlevel'); $cache->delete($this->get('contextlevel')); } } manager_observer.php 0000644 00000005611 15152024213 0010574 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class \tool_dataprivacy\manager_observer. * * @package tool_dataprivacy * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * A failure observer for the \core_privacy\manager. * * @package tool_dataprivacy * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager_observer implements \core_privacy\manager_observer { /** * Notifies all DPOs that an exception occurred. * * @param \Throwable $e * @param string $component * @param string $interface * @param string $methodname * @param array $params */ public function handle_component_failure($e, $component, $interface, $methodname, array $params) { // Get the list of the site Data Protection Officers. $dpos = api::get_site_dpos(); $messagesubject = get_string('exceptionnotificationsubject', 'tool_dataprivacy'); $a = (object)[ 'fullmethodname' => \core_privacy\manager::get_provider_classname_for_component($component) . '::' . $methodname, 'component' => $component, 'message' => $e->getMessage(), 'backtrace' => $e->getTraceAsString() ]; $messagebody = get_string('exceptionnotificationbody', 'tool_dataprivacy', $a); // Email the data request to the Data Protection Officer(s)/Admin(s). foreach ($dpos as $dpo) { $message = new \core\message\message(); $message->courseid = SITEID; $message->component = 'tool_dataprivacy'; $message->name = 'notifyexceptions'; $message->userfrom = \core_user::get_noreply_user(); $message->subject = $messagesubject; $message->fullmessageformat = FORMAT_HTML; $message->notification = 1; $message->userto = $dpo; $message->fullmessagehtml = $messagebody; $message->fullmessage = html_to_text($messagebody); // Send message. message_send($message); } } } metadata_registry.php 0000644 00000020306 15152024213 0010761 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing helper methods for processing data requests. * * @package tool_dataprivacy * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * Class containing helper methods for processing data requests. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class metadata_registry { /** * Returns plugin types / plugins and the user data that it stores in a format that can be sent to a template. * * @return array An array with all of the plugin types / plugins and the user data they store. */ public function get_registry_metadata() { $manager = new \core_privacy\manager(); $manager->set_observer(new \tool_dataprivacy\manager_observer()); $pluginman = \core_plugin_manager::instance(); $contributedplugins = $this->get_contrib_list(); $metadata = $manager->get_metadata_for_components(); $fullyrichtree = $this->get_full_component_list(); foreach ($fullyrichtree as $branch => $leaves) { $plugintype = $leaves['plugin_type']; $plugins = array_map(function($component) use ($manager, $metadata, $contributedplugins, $plugintype, $pluginman) { // Use the plugin name for the plugins, ignore for core subsystems. $internaldata = ($plugintype == 'core') ? ['component' => $component] : ['component' => $pluginman->plugin_name($component)]; $internaldata['raw_component'] = $component; if ($manager->component_is_compliant($component)) { $internaldata['compliant'] = true; if (isset($metadata[$component])) { $collection = $metadata[$component]->get_collection(); $internaldata = $this->format_metadata($collection, $component, $internaldata); } else if ($manager->is_empty_subsystem($component)) { // This is an unused subsystem. // Use the generic string. $internaldata['nullprovider'] = get_string('privacy:subsystem:empty', 'core_privacy'); } else { // Call get_reason for null provider. $internaldata['nullprovider'] = get_string($manager->get_null_provider_reason($component), $component); } } else { $internaldata['compliant'] = false; } // Check to see if we are an external plugin. // Plugin names can contain _ characters, limit to 2 to just remove initial plugintype. $componentshortname = explode('_', $component, 2); $shortname = array_pop($componentshortname); if (isset($contributedplugins[$plugintype][$shortname])) { $internaldata['external'] = true; } // Additional interface checks. if (!$manager->is_empty_subsystem($component)) { $classname = $manager->get_provider_classname_for_component($component); if (class_exists($classname)) { $componentclass = new $classname(); // Check if the interface is deprecated. if ($componentclass instanceof \core_privacy\local\deprecated) { $internaldata['deprecated'] = true; } // Check that the core_userlist_provider is implemented for all user data providers. if ($componentclass instanceof \core_privacy\local\request\core_user_data_provider && !$componentclass instanceof \core_privacy\local\request\core_userlist_provider) { $internaldata['userlistnoncompliance'] = true; } // Check that any type of userlist_provider is implemented for all shared data providers. if ($componentclass instanceof \core_privacy\local\request\shared_data_provider && !$componentclass instanceof \core_privacy\local\request\userlist_provider) { $internaldata['userlistnoncompliance'] = true; } } } return $internaldata; }, $leaves['plugins']); $fullyrichtree[$branch]['plugin_type_raw'] = $plugintype; // We're done using the plugin type. Convert it to a readable string. $fullyrichtree[$branch]['plugin_type'] = $pluginman->plugintype_name($plugintype); $fullyrichtree[$branch]['plugins'] = $plugins; } return $fullyrichtree; } /** * Formats the metadata for use with a template. * * @param array $collection The collection associated with the component that we want to expand and format. * @param string $component The component that we are dealing in * @param array $internaldata The array to add the formatted metadata to. * @return array The internal data array with the formatted metadata. */ protected function format_metadata($collection, $component, $internaldata) { foreach ($collection as $collectioninfo) { $privacyfields = $collectioninfo->get_privacy_fields(); $fields = ''; if (!empty($privacyfields)) { $fields = array_map(function($key, $field) use ($component) { return [ 'field_name' => $key, 'field_summary' => get_string($field, $component) ]; }, array_keys($privacyfields), $privacyfields); } // Can the metadata types be located somewhere else besides core? $items = explode('\\', get_class($collectioninfo)); $type = array_pop($items); $typedata = [ 'name' => $collectioninfo->get_name(), 'type' => $type, 'fields' => $fields, 'summary' => get_string($collectioninfo->get_summary(), $component) ]; if (strpos($type, 'subsystem_link') === 0 || strpos($type, 'plugintype_link') === 0) { $typedata['link'] = true; } $internaldata['metadata'][] = $typedata; } return $internaldata; } /** * Return the full list of components. * * @return array An array of plugin types which contain plugin data. */ protected function get_full_component_list() { global $CFG; $list = \core_component::get_component_list(); $list['core']['core'] = "{$CFG->dirroot}/lib"; $formattedlist = []; foreach ($list as $plugintype => $plugin) { $formattedlist[] = ['plugin_type' => $plugintype, 'plugins' => array_keys($plugin)]; } return $formattedlist; } /** * Returns a list of contributed plugins installed on the system. * * @return array A list of contributed plugins installed. */ protected function get_contrib_list() { return array_map(function($plugins) { return array_filter($plugins, function($plugindata) { return !$plugindata->is_standard(); }); }, \core_plugin_manager::instance()->get_plugins()); } } local/helper.php 0000644 00000005215 15152024213 0007624 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace report_progress\local; /** * Helper for report progress. * * @package report_progress * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ class helper { /** The default number of results to be shown per page. */ const COMPLETION_REPORT_PAGE = 25; /** * Get activities information by the activity include and activity order option. * * @param \completion_info $completion Completion information of course. * @param string $activityinclude Activity type for filtering. * @param string $activityorder Activity sort option. * @return array The available activity types and activities array after filtering and sorting. * @throws \coding_exception */ public static function get_activities_to_show(\completion_info $completion, string $activityinclude, string $activityorder): array { // Get all activity types. $activities = $completion->get_activities(); $availableactivitytypes = []; foreach ($activities as $activity) { $availableactivitytypes[$activity->modname] = $activity->get_module_type_name(true); } asort($availableactivitytypes); $availableactivitytypes = ['all' => get_string('allactivitiesandresources', 'report_progress')] + $availableactivitytypes; // Filter activities by type. if (!empty($activityinclude) && $activityinclude !== 'all') { $activities = array_filter($activities, function($activity) use ($activityinclude) { return $activity->modname === $activityinclude; }); } // The activities are sorted by activity order on course page by default. if ($activityorder === 'alphabetical') { usort($activities, function($a, $b) { return strcmp($a->name, $b->name); }); } return [$availableactivitytypes, $activities]; } } filtered_userlist.php 0000644 00000004450 15152024213 0011003 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * An implementation of a userlist which has been filtered and approved. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * An implementation of a userlist which can be filtered by role. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class filtered_userlist extends \core_privacy\local\request\approved_userlist { /** * Apply filters to only remove users in the expireduserids list, and to remove any who are in the unexpired list. * The unexpired list wins where a user is in both lists. * * @param int[] $expireduserids The list of userids for users who should be expired. * @param int[] $unexpireduserids The list of userids for those users who should not be expired. * @return $this */ public function apply_expired_context_filters(array $expireduserids, array $unexpireduserids) : filtered_userlist { // The current userlist content. $userids = $this->get_userids(); if (!empty($expireduserids)) { // Now remove any not on the list of expired users. $userids = array_intersect($userids, $expireduserids); } if (!empty($unexpireduserids)) { // Remove any on the list of unexpiredusers users. $userids = array_diff($userids, $unexpireduserids); } $this->set_userids($userids); return $this; } } expired_context.php 0000644 00000025742 15152024213 0010466 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class that represents an expired context. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use dml_exception; defined('MOODLE_INTERNAL') || die(); /** * Class that represents an expired context. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_context extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_ctxexpired'; /** * Expired contexts with no delete action scheduled. */ const STATUS_EXPIRED = 0; /** * Expired contexts approved for deletion. */ const STATUS_APPROVED = 1; /** * Already processed expired contexts. */ const STATUS_CLEANED = 2; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return [ 'contextid' => [ 'type' => PARAM_INT, 'description' => 'The context id.', ], 'defaultexpired' => [ 'type' => PARAM_INT, 'description' => 'Whether to default retention period for the purpose has been reached', 'default' => 1, ], 'expiredroles' => [ 'type' => PARAM_TEXT, 'description' => 'This list of roles to include during deletion', 'default' => '', ], 'unexpiredroles' => [ 'type' => PARAM_TEXT, 'description' => 'This list of roles to exclude during deletion', 'default' => '', ], 'status' => [ 'choices' => [ self::STATUS_EXPIRED, self::STATUS_APPROVED, self::STATUS_CLEANED, ], 'type' => PARAM_INT, 'description' => 'The deletion status of the context.', ], ]; } /** * Returns expired_contexts instances that match the provided level and status. * * @param int $contextlevel The context level filter criterion. * @param bool $status The expired context record's status. * @param string $sort The sort column. Must match the column name in {tool_dataprivacy_ctxexpired} table * @param int $offset The query offset. * @param int $limit The query limit. * @return expired_context[] * @throws dml_exception */ public static function get_records_by_contextlevel($contextlevel = null, $status = false, $sort = 'timecreated', $offset = 0, $limit = 0) { global $DB; $sql = "SELECT expiredctx.* FROM {" . self::TABLE . "} expiredctx JOIN {context} ctx ON ctx.id = expiredctx.contextid"; $params = []; $conditions = []; if (!empty($contextlevel)) { $conditions[] = "ctx.contextlevel = :contextlevel"; $params['contextlevel'] = intval($contextlevel); } if ($status !== false) { $conditions[] = "expiredctx.status = :status"; $params['status'] = intval($status); } if (!empty($conditions)) { $sql .= ' WHERE ' . implode(' AND ', $conditions); } $sql .= " ORDER BY expiredctx.{$sort}"; $records = $DB->get_records_sql($sql, $params, $offset, $limit); // We return class instances. $instances = array(); foreach ($records as $key => $record) { $instances[$key] = new static(0, $record); } return $instances; } /** * Returns the number of expired_contexts instances that match the provided level and status. * * @param int $contextlevel * @param bool $status * @return int * @throws dml_exception */ public static function get_record_count_by_contextlevel($contextlevel = null, $status = false) { global $DB; $sql = "SELECT COUNT(1) FROM {" . self::TABLE . "} expiredctx JOIN {context} ctx ON ctx.id = expiredctx.contextid"; $conditions = []; $params = []; if (!empty($contextlevel)) { $conditions[] = "ctx.contextlevel = :contextlevel"; $params['contextlevel'] = intval($contextlevel); } if ($status !== false) { $sql .= " AND expiredctx.status = :status"; $params['status'] = intval($status); } if (!empty($conditions)) { $sql .= ' WHERE ' . implode(' AND ', $conditions); } return $DB->count_records_sql($sql, $params); } /** * Set the list of role IDs for either expiredroles, or unexpiredroles. * * @param string $field * @param int[] $roleids * @return expired_context */ protected function set_roleids_for(string $field, array $roleids) : expired_context { $roledata = json_encode($roleids); $this->raw_set($field, $roledata); return $this; } /** * Get the list of role IDs for either expiredroles, or unexpiredroles. * * @param string $field * @return int[] */ protected function get_roleids_for(string $field) { $value = $this->raw_get($field); if (empty($value)) { return []; } return json_decode($value); } /** * Set the list of unexpired role IDs. * * @param int[] $roleids * @return expired_context */ protected function set_unexpiredroles(array $roleids) : expired_context { $this->set_roleids_for('unexpiredroles', $roleids); return $this; } /** * Add a set of role IDs to the list of expired role IDs. * * @param int[] $roleids * @return expired_context */ public function add_expiredroles(array $roleids) : expired_context { $existing = $this->get('expiredroles'); $newvalue = array_merge($existing, $roleids); $this->set('expiredroles', $newvalue); return $this; } /** * Add a set of role IDs to the list of unexpired role IDs. * * @param int[] $roleids * @return unexpired_context */ public function add_unexpiredroles(array $roleids) : expired_context { $existing = $this->get('unexpiredroles'); $newvalue = array_merge($existing, $roleids); $this->set('unexpiredroles', $newvalue); return $this; } /** * Set the list of expired role IDs. * * @param int[] $roleids * @return expired_context */ protected function set_expiredroles(array $roleids) : expired_context { $this->set_roleids_for('expiredroles', $roleids); return $this; } /** * Get the list of expired role IDs. * * @return int[] */ protected function get_expiredroles() { return $this->get_roleids_for('expiredroles'); } /** * Get the list of unexpired role IDs. * * @return int[] */ protected function get_unexpiredroles() { return $this->get_roleids_for('unexpiredroles'); } /** * Create a new expired_context based on the context, and expiry_info object. * * @param \context $context * @param expiry_info $info * @param boolean $save * @return expired_context */ public static function create_from_expiry_info(\context $context, expiry_info $info, bool $save = true) : expired_context { $record = (object) [ 'contextid' => $context->id, 'status' => self::STATUS_EXPIRED, 'defaultexpired' => (int) $info->is_default_expired(), ]; $expiredcontext = new static(0, $record); $expiredcontext->set('expiredroles', $info->get_expired_roles()); $expiredcontext->set('unexpiredroles', $info->get_unexpired_roles()); if ($save) { $expiredcontext->save(); } return $expiredcontext; } /** * Update the expired_context from an expiry_info object which relates to this context. * * @param expiry_info $info * @return $this */ public function update_from_expiry_info(expiry_info $info) : expired_context { $save = false; // Compare the expiredroles. $thisexpired = $this->get('expiredroles'); $infoexpired = $info->get_expired_roles(); sort($thisexpired); sort($infoexpired); if ($infoexpired != $thisexpired) { $this->set('expiredroles', $infoexpired); $save = true; } // Compare the unexpiredroles. $thisunexpired = $this->get('unexpiredroles'); $infounexpired = $info->get_unexpired_roles(); sort($thisunexpired); sort($infounexpired); if ($infounexpired != $thisunexpired) { $this->set('unexpiredroles', $infounexpired); $save = true; } if (empty($this->get('defaultexpired')) == $info->is_default_expired()) { $this->set('defaultexpired', (int) $info->is_default_expired()); $save = true; } if ($save) { $this->set('status', self::STATUS_EXPIRED); $this->save(); } return $this; } /** * Check whether this expired_context record is in a state ready for deletion to actually take place. * * @return bool */ public function can_process_deletion() : bool { return ($this->get('status') == self::STATUS_APPROVED); } /** * Check whether this expired_context record has already been cleaned. * * @return bool */ public function is_complete() : bool { return ($this->get('status') == self::STATUS_CLEANED); } /** * Whether this context has 'fully' expired. * That is to say that the default retention period has been reached, and that there are no unexpired roles. * * @return bool */ public function is_fully_expired() : bool { return $this->get('defaultexpired') && empty($this->get('unexpiredroles')); } } external/category_exporter.php 0000644 00000005015 15152024213 0012640 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting data category. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use core\external\persistent_exporter; use tool_dataprivacy\category; use tool_dataprivacy\context_instance; /** * Class for exporting field data. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category_exporter extends persistent_exporter { /** * Defines the persistent class. * * @return string */ protected static function define_class() { return \tool_dataprivacy\category::class; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return array( 'context' => 'context', ); } /** * Utility function that fetches a category name from the given ID. * * @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID. * @return string The purpose name. */ public static function get_name($categoryid) { global $PAGE; if ($categoryid === false || $categoryid == context_instance::INHERIT) { return get_string('inherit', 'tool_dataprivacy'); } else if ($categoryid == context_instance::NOTSET) { return get_string('notset', 'tool_dataprivacy'); } else { $purpose = new category($categoryid); $output = $PAGE->get_renderer('tool_dataprivacy'); $exporter = new self($purpose, ['context' => \context_system::instance()]); $data = $exporter->export($output); return $data->name; } } } external/data_request_exporter.php 0000644 00000016100 15152024213 0013501 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting user evidence with all competencies. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use coding_exception; use core\external\persistent_exporter; use core_user; use core_user\external\user_summary_exporter; use dml_exception; use moodle_exception; use renderer_base; use tool_dataprivacy\api; use tool_dataprivacy\data_request; use tool_dataprivacy\local\helper; /** * Class for exporting user evidence with all competencies. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_request_exporter extends persistent_exporter { /** * Class definition. * * @return string */ protected static function define_class() { return data_request::class; } /** * Related objects definition. * * @return array */ protected static function define_related() { return [ 'context' => 'context', ]; } /** * Other properties definition. * * @return array */ protected static function define_other_properties() { return [ 'foruser' => [ 'type' => user_summary_exporter::read_properties_definition(), ], 'requestedbyuser' => [ 'type' => user_summary_exporter::read_properties_definition(), 'optional' => true ], 'dpouser' => [ 'type' => user_summary_exporter::read_properties_definition(), 'optional' => true ], 'messagehtml' => [ 'type' => PARAM_RAW, 'optional' => true ], 'typename' => [ 'type' => PARAM_TEXT, ], 'typenameshort' => [ 'type' => PARAM_TEXT, ], 'statuslabel' => [ 'type' => PARAM_TEXT, ], 'statuslabelclass' => [ 'type' => PARAM_TEXT, ], 'canreview' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => false ], 'approvedeny' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => false ], 'canmarkcomplete' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => false ], ]; } /** * Assign values to the defined other properties. * * @param renderer_base $output The output renderer object. * @return array * @throws coding_exception * @throws dml_exception * @throws moodle_exception */ protected function get_other_values(renderer_base $output) { $values = []; $foruserid = $this->persistent->get('userid'); $user = core_user::get_user($foruserid, '*', MUST_EXIST); $userexporter = new user_summary_exporter($user); $values['foruser'] = $userexporter->export($output); $requestedbyid = $this->persistent->get('requestedby'); if ($requestedbyid != $foruserid) { $user = core_user::get_user($requestedbyid, '*', MUST_EXIST); $userexporter = new user_summary_exporter($user); $values['requestedbyuser'] = $userexporter->export($output); } else { $values['requestedbyuser'] = $values['foruser']; } if (!empty($this->persistent->get('dpo'))) { $dpoid = $this->persistent->get('dpo'); $user = core_user::get_user($dpoid, '*', MUST_EXIST); $userexporter = new user_summary_exporter($user); $values['dpouser'] = $userexporter->export($output); } $values['messagehtml'] = text_to_html($this->persistent->get('comments')); $requesttype = $this->persistent->get('type'); $values['typename'] = helper::get_request_type_string($requesttype); $values['typenameshort'] = helper::get_shortened_request_type_string($requesttype); $values['canreview'] = false; $values['approvedeny'] = false; $values['statuslabel'] = helper::get_request_status_string($this->persistent->get('status')); switch ($this->persistent->get('status')) { case api::DATAREQUEST_STATUS_PENDING: $values['statuslabelclass'] = 'badge-info'; // Request can be manually completed for general enquiry requests. $values['canmarkcomplete'] = $requesttype == api::DATAREQUEST_TYPE_OTHERS; break; case api::DATAREQUEST_STATUS_AWAITING_APPROVAL: $values['statuslabelclass'] = 'badge-info'; // DPO can review the request once it's ready. $values['canreview'] = true; // Whether the DPO can approve or deny the request. $values['approvedeny'] = in_array($requesttype, [api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE]); // If the request's type is delete, check if user have permission to approve/deny it. if ($requesttype == api::DATAREQUEST_TYPE_DELETE) { $values['approvedeny'] = api::can_create_data_deletion_request_for_other(); } break; case api::DATAREQUEST_STATUS_APPROVED: $values['statuslabelclass'] = 'badge-info'; break; case api::DATAREQUEST_STATUS_PROCESSING: $values['statuslabelclass'] = 'badge-info'; break; case api::DATAREQUEST_STATUS_COMPLETE: case api::DATAREQUEST_STATUS_DOWNLOAD_READY: case api::DATAREQUEST_STATUS_DELETED: $values['statuslabelclass'] = 'badge-success'; break; case api::DATAREQUEST_STATUS_CANCELLED: $values['statuslabelclass'] = 'badge-warning'; break; case api::DATAREQUEST_STATUS_REJECTED: $values['statuslabelclass'] = 'badge-danger'; break; case api::DATAREQUEST_STATUS_EXPIRED: $values['statuslabelclass'] = 'badge-secondary'; break; } return $values; } } external/context_instance_exporter.php 0000644 00000002603 15152024213 0014373 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting context instance. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use core\external\persistent_exporter; /** * Class for exporting context instance. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context_instance_exporter extends persistent_exporter { /** * Defines the persistent class. * * @return string */ protected static function define_class() { return \tool_dataprivacy\context_instance::class; } } external/name_description_exporter.php 0000644 00000003410 15152024213 0014343 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting an object containing a name and a description. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class that exports an object containing a name and a description. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class name_description_exporter extends exporter { /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return array( 'context' => 'context', ); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'name' => [ 'type' => PARAM_TEXT, ], 'description' => [ 'type' => PARAM_TEXT, ], ]; } } external/purpose_exporter.php 0000644 00000012440 15152024213 0012520 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting data purpose. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use coding_exception; use core\external\persistent_exporter; use Exception; use renderer_base; use tool_dataprivacy\context_instance; use tool_dataprivacy\purpose; /** * Class for exporting field data. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose_exporter extends persistent_exporter { /** * Defines the persistent class. * * @return string */ protected static function define_class() { return purpose::class; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return array( 'context' => 'context', ); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'formattedretentionperiod' => [ 'type' => PARAM_TEXT ], 'formattedlawfulbases' => [ 'type' => name_description_exporter::read_properties_definition(), 'multiple' => true ], 'formattedsensitivedatareasons' => [ 'type' => name_description_exporter::read_properties_definition(), 'multiple' => true, 'optional' => true ], 'roleoverrides' => [ 'type' => PARAM_TEXT ], ]; } /** * Return other properties. * * @param renderer_base $output * @return array * @throws coding_exception * @throws Exception */ protected function get_other_values(renderer_base $output) { $values = []; $formattedbases = []; $lawfulbases = explode(',', $this->persistent->get('lawfulbases')); if (!empty($lawfulbases)) { foreach ($lawfulbases as $basis) { if (empty(trim($basis))) { continue; } $formattedbases[] = (object)[ 'name' => get_string($basis . '_name', 'tool_dataprivacy'), 'description' => get_string($basis . '_description', 'tool_dataprivacy') ]; } } $values['formattedlawfulbases'] = $formattedbases; $formattedsensitivereasons = []; $sensitivereasons = explode(',', $this->persistent->get('sensitivedatareasons') ?? ''); if (!empty($sensitivereasons)) { foreach ($sensitivereasons as $reason) { if (empty(trim($reason))) { continue; } $formattedsensitivereasons[] = (object)[ 'name' => get_string($reason . '_name', 'tool_dataprivacy'), 'description' => get_string($reason . '_description', 'tool_dataprivacy') ]; } } $values['formattedsensitivedatareasons'] = $formattedsensitivereasons; $retentionperiod = $this->persistent->get('retentionperiod'); if ($retentionperiod) { $formattedtime = \tool_dataprivacy\api::format_retention_period(new \DateInterval($retentionperiod)); } else { $formattedtime = get_string('retentionperiodnotdefined', 'tool_dataprivacy'); } $values['formattedretentionperiod'] = $formattedtime; $values['roleoverrides'] = !empty($this->persistent->get_purpose_overrides()); return $values; } /** * Utility function that fetches a purpose name from the given ID. * * @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID. * @return string The purpose name. */ public static function get_name($purposeid) { global $PAGE; if ($purposeid === false || $purposeid == context_instance::INHERIT) { return get_string('inherit', 'tool_dataprivacy'); } else if ($purposeid == context_instance::NOTSET) { return get_string('notset', 'tool_dataprivacy'); } else { $purpose = new purpose($purposeid); $output = $PAGE->get_renderer('tool_dataprivacy'); $exporter = new self($purpose, ['context' => \context_system::instance()]); $data = $exporter->export($output); return $data->name; } } } expired_contexts_manager.php 0000644 00000115125 15152024213 0012336 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Expired contexts manager. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use core_privacy\manager; use tool_dataprivacy\expired_context; defined('MOODLE_INTERNAL') || die(); /** * Expired contexts manager. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_contexts_manager { /** * Number of deleted contexts for each scheduled task run. */ const DELETE_LIMIT = 200; /** @var progress_trace The log progress tracer */ protected $progresstracer = null; /** @var manager The privacy manager */ protected $manager = null; /** @var \progress_trace Trace tool for logging */ protected $trace = null; /** * Constructor for the expired_contexts_manager. * * @param \progress_trace $trace */ public function __construct(\progress_trace $trace = null) { if (null === $trace) { $trace = new \null_progress_trace(); } $this->trace = $trace; } /** * Flag expired contexts as expired. * * @return int[] The number of contexts flagged as expired for courses, and users. */ public function flag_expired_contexts() : array { $this->trace->output('Checking requirements'); if (!$this->check_requirements()) { $this->trace->output('Requirements not met. Cannot process expired retentions.', 1); return [0, 0]; } // Clear old and stale records first. $this->trace->output('Clearing obselete records.', 0); static::clear_old_records(); $this->trace->output('Done.', 1); $this->trace->output('Calculating potential course expiries.', 0); $data = static::get_nested_expiry_info_for_courses(); $coursecount = 0; $this->trace->output('Updating course expiry data.', 0); foreach ($data as $expiryrecord) { if ($this->update_from_expiry_info($expiryrecord)) { $coursecount++; } } $this->trace->output('Done.', 1); $this->trace->output('Calculating potential user expiries.', 0); $data = static::get_nested_expiry_info_for_user(); $usercount = 0; $this->trace->output('Updating user expiry data.', 0); foreach ($data as $expiryrecord) { if ($this->update_from_expiry_info($expiryrecord)) { $usercount++; } } $this->trace->output('Done.', 1); return [$coursecount, $usercount]; } /** * Clear old and stale records. */ protected static function clear_old_records() { global $DB; $sql = "SELECT dpctx.* FROM {tool_dataprivacy_ctxexpired} dpctx LEFT JOIN {context} ctx ON ctx.id = dpctx.contextid WHERE ctx.id IS NULL"; $orphaned = $DB->get_recordset_sql($sql); foreach ($orphaned as $orphan) { $expiredcontext = new expired_context(0, $orphan); $expiredcontext->delete(); } // Delete any child of a user context. $parentpath = $DB->sql_concat('ctxuser.path', "'/%'"); $params = [ 'contextuser' => CONTEXT_USER, ]; $sql = "SELECT dpctx.* FROM {tool_dataprivacy_ctxexpired} dpctx WHERE dpctx.contextid IN ( SELECT ctx.id FROM {context} ctxuser JOIN {context} ctx ON ctx.path LIKE {$parentpath} WHERE ctxuser.contextlevel = :contextuser )"; $userchildren = $DB->get_recordset_sql($sql, $params); foreach ($userchildren as $child) { $expiredcontext = new expired_context(0, $child); $expiredcontext->delete(); } } /** * Get the full nested set of expiry data relating to all contexts. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info($contextpath = '') : array { $coursepaths = self::get_nested_expiry_info_for_courses($contextpath); $userpaths = self::get_nested_expiry_info_for_user($contextpath); return array_merge($coursepaths, $userpaths); } /** * Get the full nested set of expiry data relating to course-related contexts. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info_for_courses($contextpath = '') : array { global $DB; $contextfields = \context_helper::get_preload_record_columns_sql('ctx'); $expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx'); $purposefields = 'dpctx.purposeid'; $coursefields = 'ctxcourse.expirydate AS expirydate'; $fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $coursefields, $purposefields]); // We want all contexts at course-dependant levels. $parentpath = $DB->sql_concat('ctxcourse.path', "'/%'"); // This SQL query returns all course-dependant contexts (including the course context) // which course end date already passed. // This is ordered by the context path in reverse order, which will give the child nodes before any parent node. $params = [ 'contextlevel' => CONTEXT_COURSE, ]; $where = ''; if (!empty($contextpath)) { $where = "WHERE (ctx.path = :pathmatchexact OR ctx.path LIKE :pathmatchchildren)"; $params['pathmatchexact'] = $contextpath; $params['pathmatchchildren'] = "{$contextpath}/%"; } $sql = "SELECT $fields FROM {context} ctx JOIN ( SELECT c.enddate AS expirydate, subctx.path FROM {context} subctx JOIN {course} c ON subctx.contextlevel = :contextlevel AND subctx.instanceid = c.id AND c.format != 'site' ) ctxcourse ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx ON dpctx.contextid = ctx.id LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid {$where} ORDER BY ctx.path DESC"; return self::get_nested_expiry_info_from_sql($sql, $params); } /** * Get the full nested set of expiry data. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info_for_user($contextpath = '') : array { global $DB; $contextfields = \context_helper::get_preload_record_columns_sql('ctx'); $expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx'); $purposefields = 'dpctx.purposeid'; $userfields = 'u.lastaccess AS expirydate'; $fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $userfields, $purposefields]); // We want all contexts at user-dependant levels. $parentpath = $DB->sql_concat('ctxuser.path', "'/%'"); // This SQL query returns all user-dependant contexts (including the user context) // This is ordered by the context path in reverse order, which will give the child nodes before any parent node. $params = [ 'contextlevel' => CONTEXT_USER, ]; $where = ''; if (!empty($contextpath)) { $where = "AND ctx.path = :pathmatchexact"; $params['pathmatchexact'] = $contextpath; } $sql = "SELECT $fields, u.deleted AS userdeleted FROM {context} ctx JOIN {user} u ON ctx.instanceid = u.id LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx ON dpctx.contextid = ctx.id LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid WHERE ctx.contextlevel = :contextlevel {$where} ORDER BY ctx.path DESC"; return self::get_nested_expiry_info_from_sql($sql, $params); } /** * Get the full nested set of expiry data given appropriate SQL. * Only contexts which have expired will be included. * * @param string $sql The SQL used to select the nested information. * @param array $params The params required by the SQL. * @return \stdClass[] */ protected static function get_nested_expiry_info_from_sql(string $sql, array $params) : array { global $DB; $fulllist = $DB->get_recordset_sql($sql, $params); $datalist = []; $expiredcontents = []; $pathstoskip = []; $userpurpose = data_registry::get_effective_contextlevel_value(CONTEXT_USER, 'purpose'); foreach ($fulllist as $record) { \context_helper::preload_from_record($record); $context = \context::instance_by_id($record->id, false); if (!self::is_eligible_for_deletion($pathstoskip, $context)) { // We should skip this context, and therefore all of it's children. $datalist = array_filter($datalist, function($data, $path) use ($context) { // Remove any child of this context. // Technically this should never be fulfilled because the query is ordered in path DESC, but is kept // in to be certain. return (false === strpos($path, "{$context->path}/")); }, ARRAY_FILTER_USE_BOTH); if ($record->expiredctxid) { // There was previously an expired context record. // Delete it to be on the safe side. $expiredcontext = new expired_context(null, expired_context::extract_record($record, 'expiredctx')); $expiredcontext->delete(); } continue; } if ($context instanceof \context_user) { $purpose = $userpurpose; } else { $purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET; $purpose = api::get_effective_context_purpose($context, $purposevalue); } if ($context instanceof \context_user && !empty($record->userdeleted)) { $expiryinfo = static::get_expiry_info($purpose, $record->userdeleted); } else { $expiryinfo = static::get_expiry_info($purpose, $record->expirydate); } foreach ($datalist as $path => $data) { // Merge with already-processed children. if (strpos($path, $context->path) !== 0) { continue; } $expiryinfo->merge_with_child($data->info); } $datalist[$context->path] = (object) [ 'context' => $context, 'record' => $record, 'purpose' => $purpose, 'info' => $expiryinfo, ]; } $fulllist->close(); return $datalist; } /** * Check whether the supplied context would be elible for deletion. * * @param array $pathstoskip A set of paths which should be skipped * @param \context $context * @return bool */ protected static function is_eligible_for_deletion(array &$pathstoskip, \context $context) : bool { $shouldskip = false; // Check whether any of the child contexts are ineligble. $shouldskip = !empty(array_filter($pathstoskip, function($path) use ($context) { // If any child context has already been skipped then it will appear in this list. // Since paths include parents, test if the context under test appears as the haystack in the skipped // context's needle. return false !== (strpos($context->path, $path)); })); if (!$shouldskip && $context instanceof \context_user) { $shouldskip = !self::are_user_context_dependencies_expired($context); } if ($shouldskip) { // Add this to the list of contexts to skip for parentage checks. $pathstoskip[] = $context->path; } return !$shouldskip; } /** * Deletes the expired contexts. * * @return int[] The number of deleted contexts. */ public function process_approved_deletions() : array { $this->trace->output('Checking requirements'); if (!$this->check_requirements()) { $this->trace->output('Requirements not met. Cannot process expired retentions.', 1); return [0, 0]; } $this->trace->output('Fetching all approved and expired contexts for deletion.'); $expiredcontexts = expired_context::get_records(['status' => expired_context::STATUS_APPROVED]); $this->trace->output('Done.', 1); $totalprocessed = 0; $usercount = 0; $coursecount = 0; foreach ($expiredcontexts as $expiredctx) { $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING); if (empty($context)) { // Unable to process this request further. // We have no context to delete. $expiredctx->delete(); continue; } $this->trace->output("Deleting data for " . $context->get_context_name(), 2); if ($this->delete_expired_context($expiredctx)) { $this->trace->output("Done.", 3); if ($context instanceof \context_user) { $usercount++; } else { $coursecount++; } $totalprocessed++; if ($totalprocessed >= $this->get_delete_limit()) { break; } } } return [$coursecount, $usercount]; } /** * Deletes user data from the provided context. * * @param expired_context $expiredctx * @return \context|false */ protected function delete_expired_context(expired_context $expiredctx) { $context = \context::instance_by_id($expiredctx->get('contextid')); $this->get_progress()->output("Deleting context {$context->id} - " . $context->get_context_name(true, true)); // Update the expired_context and verify that it is still ready for deletion. $expiredctx = $this->update_expired_context($expiredctx); if (empty($expiredctx)) { $this->get_progress()->output("Context has changed since approval and is no longer pending approval. Skipping", 1); return false; } if (!$expiredctx->can_process_deletion()) { // This only happens if the record was updated after being first fetched. $this->get_progress()->output("Context has changed since approval and must be re-approved. Skipping", 1); $expiredctx->set('status', expired_context::STATUS_EXPIRED); $expiredctx->save(); return false; } $privacymanager = $this->get_privacy_manager(); if ($expiredctx->is_fully_expired()) { if ($context instanceof \context_user) { $this->delete_expired_user_context($expiredctx); } else { // This context is fully expired - that is that the default retention period has been reached, and there are // no remaining overrides. $privacymanager->delete_data_for_all_users_in_context($context); } // Mark the record as cleaned. $expiredctx->set('status', expired_context::STATUS_CLEANED); $expiredctx->save(); return $context; } // We need to find all users in the context, and delete just those who have expired. $collection = $privacymanager->get_users_in_context($context); // Apply the expired and unexpired filters to remove the users in these categories. $userassignments = $this->get_role_users_for_expired_context($expiredctx, $context); $approvedcollection = new \core_privacy\local\request\userlist_collection($context); foreach ($collection as $pendinguserlist) { $userlist = filtered_userlist::create_from_userlist($pendinguserlist); $userlist->apply_expired_context_filters($userassignments->expired, $userassignments->unexpired); if (count($userlist)) { $approvedcollection->add_userlist($userlist); } } if (count($approvedcollection)) { // Perform the deletion with the newly approved collection. $privacymanager->delete_data_for_users_in_context($approvedcollection); } // Mark the record as cleaned. $expiredctx->set('status', expired_context::STATUS_CLEANED); $expiredctx->save(); return $context; } /** * Deletes user data from the provided user context. * * @param expired_context $expiredctx */ protected function delete_expired_user_context(expired_context $expiredctx) { global $DB; $contextid = $expiredctx->get('contextid'); $context = \context::instance_by_id($contextid); $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST); $privacymanager = $this->get_privacy_manager(); // Delete all child contexts of the user context. $parentpath = $DB->sql_concat('ctxuser.path', "'/%'"); $params = [ 'contextlevel' => CONTEXT_USER, 'contextid' => $expiredctx->get('contextid'), ]; $fields = \context_helper::get_preload_record_columns_sql('ctx'); $sql = "SELECT ctx.id, $fields FROM {context} ctxuser JOIN {context} ctx ON ctx.path LIKE {$parentpath} WHERE ctxuser.contextlevel = :contextlevel AND ctxuser.id = :contextid ORDER BY ctx.path DESC"; $children = $DB->get_recordset_sql($sql, $params); foreach ($children as $child) { \context_helper::preload_from_record($child); $context = \context::instance_by_id($child->id); $privacymanager->delete_data_for_all_users_in_context($context); } $children->close(); // Delete all unprotected data that the user holds. $approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id); $contextlistcollection = $privacymanager->get_contexts_for_userid($user->id); foreach ($contextlistcollection as $contextlist) { $contextids = []; $approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist( $user, $contextlist->get_component(), $contextlist->get_contextids() )); } $privacymanager->delete_data_for_user($approvedlistcollection, $this->get_progress()); // Delete the user context. $context = \context::instance_by_id($expiredctx->get('contextid')); $privacymanager->delete_data_for_all_users_in_context($context); // This user is now fully expired - finish by deleting the user. delete_user($user); } /** * Whether end dates are required on all courses in order for a user to be expired from them. * * @return bool */ protected static function require_all_end_dates_for_user_deletion() : bool { $requireenddate = get_config('tool_dataprivacy', 'requireallenddatesforuserdeletion'); return !empty($requireenddate); } /** * Check that the requirements to start deleting contexts are satisified. * * @return bool */ protected function check_requirements() { if (!data_registry::defaults_set()) { return false; } return true; } /** * Check whether a date is beyond the specified period. * * @param string $period The Expiry Period * @param int $comparisondate The date for comparison * @return bool */ protected static function has_expired(string $period, int $comparisondate) : bool { $dt = new \DateTime(); $dt->setTimestamp($comparisondate); $dt->add(new \DateInterval($period)); return (time() >= $dt->getTimestamp()); } /** * Get the expiry info object for the specified purpose and comparison date. * * @param purpose $purpose The purpose of this context * @param int $comparisondate The date for comparison * @return expiry_info */ protected static function get_expiry_info(purpose $purpose, int $comparisondate = 0) : expiry_info { $overrides = $purpose->get_purpose_overrides(); $expiredroles = $unexpiredroles = []; if (empty($overrides)) { // There are no overrides for this purpose. if (empty($comparisondate)) { // The date is empty, therefore this context cannot be considered for automatic expiry. $defaultexpired = false; } else { $defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate); } return new expiry_info($defaultexpired, $purpose->get('protected'), [], [], []); } else { $protectedroles = []; foreach ($overrides as $override) { if (static::has_expired($override->get('retentionperiod'), $comparisondate)) { // This role has expired. $expiredroles[] = $override->get('roleid'); } else { // This role has not yet expired. $unexpiredroles[] = $override->get('roleid'); if ($override->get('protected')) { $protectedroles[$override->get('roleid')] = true; } } } $defaultexpired = false; if (static::has_expired($purpose->get('retentionperiod'), $comparisondate)) { $defaultexpired = true; } if ($defaultexpired) { $expiredroles = []; } return new expiry_info($defaultexpired, $purpose->get('protected'), $expiredroles, $unexpiredroles, $protectedroles); } } /** * Update or delete the expired_context from the expiry_info object. * This function depends upon the data structure returned from get_nested_expiry_info. * * If the context is expired in any way, then an expired_context will be returned, otherwise null will be returned. * * @param \stdClass $expiryrecord * @return expired_context|null */ protected function update_from_expiry_info(\stdClass $expiryrecord) { if ($isanyexpired = $expiryrecord->info->is_any_expired()) { // The context is expired in some fashion. // Create or update as required. if ($expiryrecord->record->expiredctxid) { $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx')); $expiredcontext->update_from_expiry_info($expiryrecord->info); if ($expiredcontext->is_complete()) { return null; } } else { $expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info); } if ($expiryrecord->context instanceof \context_user) { $userassignments = $this->get_role_users_for_expired_context($expiredcontext, $expiryrecord->context); if (!empty($userassignments->unexpired)) { $expiredcontext->delete(); return null; } } return $expiredcontext; } else { // The context is not expired. if ($expiryrecord->record->expiredctxid) { // There was previously an expired context record, but it is no longer relevant. // Delete it to be on the safe side. $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx')); $expiredcontext->delete(); } return null; } } /** * Update the expired context record. * * Note: You should use the return value as the provided value will be used to fetch data only. * * @param expired_context $expiredctx The record to update * @return expired_context|null */ protected function update_expired_context(expired_context $expiredctx) { // Fetch the context from the expired_context record. $context = \context::instance_by_id($expiredctx->get('contextid')); // Fetch the current nested expiry data. $expiryrecords = self::get_nested_expiry_info($context->path); if (empty($expiryrecords[$context->path])) { $expiredctx->delete(); return null; } // Refresh the record. // Note: Use the returned expiredctx. $expiredctx = $this->update_from_expiry_info($expiryrecords[$context->path]); if (empty($expiredctx)) { return null; } if (!$context instanceof \context_user) { // Where the target context is not a user, we check all children of the context. // The expiryrecords array only contains children, fetched from the get_nested_expiry_info call above. // No need to check that these _are_ children. foreach ($expiryrecords as $expiryrecord) { if ($expiryrecord->context->id === $context->id) { // This is record for the context being tested that we checked earlier. continue; } if (empty($expiryrecord->record->expiredctxid)) { // There is no expired context record for this context. // If there is no record, then this context cannot have been approved for removal. return null; } // Fetch the expired_context object for this record. // This needs to be updated from the expiry_info data too as there may be child changes to consider. $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx')); $expiredcontext->update_from_expiry_info($expiryrecord->info); if (!$expiredcontext->is_complete()) { return null; } } } return $expiredctx; } /** * Get the list of actual users for the combination of expired, and unexpired roles. * * @param expired_context $expiredctx * @param \context $context * @return \stdClass */ protected function get_role_users_for_expired_context(expired_context $expiredctx, \context $context) : \stdClass { $expiredroles = $expiredctx->get('expiredroles'); $expiredroleusers = []; if (!empty($expiredroles)) { // Find the list of expired role users. $expiredroleuserassignments = get_role_users($expiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id'); $expiredroleusers = array_map(function($assignment) { return $assignment->userid; }, $expiredroleuserassignments); } $expiredroleusers = array_unique($expiredroleusers); $unexpiredroles = $expiredctx->get('unexpiredroles'); $unexpiredroleusers = []; if (!empty($unexpiredroles)) { // Find the list of unexpired role users. $unexpiredroleuserassignments = get_role_users($unexpiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id'); $unexpiredroleusers = array_map(function($assignment) { return $assignment->userid; }, $unexpiredroleuserassignments); } $unexpiredroleusers = array_unique($unexpiredroleusers); if (!$expiredctx->get('defaultexpired')) { $tofilter = get_users_roles($context, $expiredroleusers); $tofilter = array_filter($tofilter, function($userroles) use ($expiredroles) { // Each iteration contains the list of role assignment for a specific user. // All roles that the user holds must match those in the list of expired roles. foreach ($userroles as $ra) { if (false === array_search($ra->roleid, $expiredroles)) { // This role was not found in the list of assignments. return true; } } return false; }); $unexpiredroleusers = array_merge($unexpiredroleusers, array_keys($tofilter)); } return (object) [ 'expired' => $expiredroleusers, 'unexpired' => $unexpiredroleusers, ]; } /** * Determine whether the supplied context has expired. * * @param \context $context * @return bool */ public static function is_context_expired(\context $context) : bool { $parents = $context->get_parent_contexts(true); foreach ($parents as $parent) { if ($parent instanceof \context_course) { // This is a context within a course. Check whether _this context_ is expired as a function of a course. return self::is_course_context_expired($context); } if ($parent instanceof \context_user) { // This is a context within a user. Check whether the _user_ has expired. return self::are_user_context_dependencies_expired($parent); } } return false; } /** * Check whether the course has expired. * * @param \stdClass $course * @return bool */ protected static function is_course_expired(\stdClass $course) : bool { $context = \context_course::instance($course->id); return self::is_course_context_expired($context); } /** * Determine whether the supplied course-related context has expired. * Note: This is not necessarily a _course_ context, but a context which is _within_ a course. * * @param \context $context * @return bool */ protected static function is_course_context_expired(\context $context) : bool { $expiryrecords = self::get_nested_expiry_info_for_courses($context->path); return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired(); } /** * Determine whether the supplied user context's dependencies have expired. * * This checks whether courses have expired, and some other check, but does not check whether the user themself has expired. * * Although this seems unusual at first, each location calling this actually checks whether the user is elgible for * deletion, irrespective if they have actually expired. * * For example, a request to delete the user only cares about course dependencies and the user's lack of expiry * should not block their own request to be deleted; whilst the expiry eligibility check has already tested for the * user being expired. * * @param \context_user $context * @return bool */ protected static function are_user_context_dependencies_expired(\context_user $context) : bool { // The context instanceid is the user's ID. if (isguestuser($context->instanceid) || is_siteadmin($context->instanceid)) { // This is an admin, or the guest and cannot expire. return false; } $courses = enrol_get_users_courses($context->instanceid, false, ['enddate']); $requireenddate = self::require_all_end_dates_for_user_deletion(); $expired = true; foreach ($courses as $course) { if (empty($course->enddate)) { // This course has no end date. if ($requireenddate) { // Course end dates are required, and this course has no end date. $expired = false; break; } // Course end dates are not required. The subsequent checks are pointless at this time so just // skip them. continue; } if ($course->enddate >= time()) { // This course is still in the future. $expired = false; break; } // This course has an end date which is in the past. if (!self::is_course_expired($course)) { // This course has not expired yet. $expired = false; break; } } return $expired; } /** * Determine whether the supplied context has expired or unprotected for the specified user. * * @param \context $context * @param \stdClass $user * @return bool */ public static function is_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) : bool { // User/course contexts can't expire if no purpose is set in the system context. if (!data_registry::defaults_set()) { return false; } $parents = $context->get_parent_contexts(true); foreach ($parents as $parent) { if ($parent instanceof \context_course) { // This is a context within a course. Check whether _this context_ is expired as a function of a course. return self::is_course_context_expired_or_unprotected_for_user($context, $user); } if ($parent instanceof \context_user) { // This is a context within a user. Check whether the _user_ has expired. return self::are_user_context_dependencies_expired($parent); } } return false; } /** * Determine whether the supplied course-related context has expired, or is unprotected. * Note: This is not necessarily a _course_ context, but a context which is _within_ a course. * * @param \context $context * @param \stdClass $user * @return bool */ protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) { if ($context->get_course_context()->instanceid == SITEID) { // The is an activity in the site course (front page). $purpose = data_registry::get_effective_contextlevel_value(CONTEXT_SYSTEM, 'purpose'); $info = static::get_expiry_info($purpose); } else { $expiryrecords = self::get_nested_expiry_info_for_courses($context->path); $info = $expiryrecords[$context->path]->info; } if ($info->is_fully_expired()) { // This context is fully expired. return true; } // Now perform user checks. $userroles = array_map(function($assignment) { return $assignment->roleid; }, get_user_roles($context, $user->id)); $unexpiredprotectedroles = $info->get_unexpired_protected_roles(); if (!empty(array_intersect($unexpiredprotectedroles, $userroles))) { // The user holds an unexpired and protected role. return false; } $unprotectedoverriddenroles = $info->get_unprotected_overridden_roles(); $matchingroles = array_intersect($unprotectedoverriddenroles, $userroles); if (!empty($matchingroles)) { // This user has at least one overridden role which is not a protected. // However, All such roles must match. // If the user has multiple roles then all must be expired, otherwise we should fall back to the default behaviour. if (empty(array_diff($userroles, $unprotectedoverriddenroles))) { // All roles that this user holds are a combination of expired, or unprotected. return true; } } if ($info->is_default_expired()) { // If the user has no unexpired roles, and the context is expired by default then this must be expired. return true; } return !$info->is_default_protected(); } /** * Create a new instance of the privacy manager. * * @return manager */ protected function get_privacy_manager() : manager { if (null === $this->manager) { $this->manager = new manager(); $this->manager->set_observer(new \tool_dataprivacy\manager_observer()); } return $this->manager; } /** * Fetch the limit for the maximum number of contexts to delete in one session. * * @return int */ protected function get_delete_limit() : int { return self::DELETE_LIMIT; } /** * Get the progress tracer. * * @return \progress_trace */ protected function get_progress() : \progress_trace { if (null === $this->progresstracer) { $this->set_progress(new \text_progress_trace()); } return $this->progresstracer; } /** * Set a specific tracer for the task. * * @param \progress_trace $trace * @return $this */ public function set_progress(\progress_trace $trace) : expired_contexts_manager { $this->progresstracer = $trace; return $this; } } processor.php 0000644 00000027017 15152024723 0007304 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * File containing processor class. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/csvlib.class.php'); /** * Processor class. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_uploadcourse_processor { /** * Create courses that do not exist yet. */ const MODE_CREATE_NEW = 1; /** * Create all courses, appending a suffix to the shortname if the course exists. */ const MODE_CREATE_ALL = 2; /** * Create courses, and update the ones that already exist. */ const MODE_CREATE_OR_UPDATE = 3; /** * Only update existing courses. */ const MODE_UPDATE_ONLY = 4; /** * During update, do not update anything... O_o Huh?! */ const UPDATE_NOTHING = 0; /** * During update, only use data passed from the CSV. */ const UPDATE_ALL_WITH_DATA_ONLY = 1; /** * During update, use either data from the CSV, or defaults. */ const UPDATE_ALL_WITH_DATA_OR_DEFAUTLS = 2; /** * During update, update missing values from either data from the CSV, or defaults. */ const UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS = 3; /** @var int processor mode. */ protected $mode; /** @var int upload mode. */ protected $updatemode; /** @var bool are renames allowed. */ protected $allowrenames = false; /** @var bool are deletes allowed. */ protected $allowdeletes = false; /** @var bool are resets allowed. */ protected $allowresets = false; /** @var string path to a restore file. */ protected $restorefile; /** @var string shortname of the course to be restored. */ protected $templatecourse; /** @var string reset courses after processing them. */ protected $reset; /** @var string template to generate a course shortname. */ protected $shortnametemplate; /** @var csv_import_reader */ protected $cir; /** @var array default values. */ protected $defaults = array(); /** @var array CSV columns. */ protected $columns = array(); /** @var array of errors where the key is the line number. */ protected $errors = array(); /** @var int line number. */ protected $linenb = 0; /** @var bool whether the process has been started or not. */ protected $processstarted = false; /** * Constructor * * @param csv_import_reader $cir import reader object * @param array $options options of the process * @param array $defaults default data value */ public function __construct(csv_import_reader $cir, array $options, array $defaults = array()) { if (!isset($options['mode']) || !in_array($options['mode'], array(self::MODE_CREATE_NEW, self::MODE_CREATE_ALL, self::MODE_CREATE_OR_UPDATE, self::MODE_UPDATE_ONLY))) { throw new coding_exception('Unknown process mode'); } // Force int to make sure === comparison work as expected. $this->mode = (int) $options['mode']; $this->updatemode = self::UPDATE_NOTHING; if (isset($options['updatemode'])) { // Force int to make sure === comparison work as expected. $this->updatemode = (int) $options['updatemode']; } if (isset($options['allowrenames'])) { $this->allowrenames = $options['allowrenames']; } if (isset($options['allowdeletes'])) { $this->allowdeletes = $options['allowdeletes']; } if (isset($options['allowresets'])) { $this->allowresets = $options['allowresets']; } if (isset($options['restorefile'])) { $this->restorefile = $options['restorefile']; } if (isset($options['templatecourse'])) { $this->templatecourse = $options['templatecourse']; } if (isset($options['reset'])) { $this->reset = $options['reset']; } if (isset($options['shortnametemplate'])) { $this->shortnametemplate = $options['shortnametemplate']; } $this->cir = $cir; $this->columns = $cir->get_columns(); $this->defaults = $defaults; $this->validate(); $this->reset(); } /** * Execute the process. * * @param object $tracker the output tracker to use. * @return void */ public function execute($tracker = null) { if ($this->processstarted) { throw new coding_exception('Process has already been started'); } $this->processstarted = true; if (empty($tracker)) { $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT); } $tracker->start(); $total = 0; $created = 0; $updated = 0; $deleted = 0; $errors = 0; // We will most certainly need extra time and memory to process big files. core_php_time_limit::raise(); raise_memory_limit(MEMORY_EXTRA); // Loop over the CSV lines. while ($line = $this->cir->next()) { $this->linenb++; $total++; $data = $this->parse_line($line); $course = $this->get_course($data); if ($course->prepare()) { $course->proceed(); $status = $course->get_statuses(); if (array_key_exists('coursecreated', $status)) { $created++; } else if (array_key_exists('courseupdated', $status)) { $updated++; } else if (array_key_exists('coursedeleted', $status)) { $deleted++; } $data = array_merge($data, $course->get_data(), array('id' => $course->get_id())); $tracker->output($this->linenb, true, $status, $data); if ($course->has_errors()) { $errors++; $tracker->output($this->linenb, false, $course->get_errors(), $data); } } else { $errors++; $tracker->output($this->linenb, false, $course->get_errors(), $data); } } $tracker->finish(); $tracker->results($total, $created, $updated, $deleted, $errors); } /** * Return a course import object. * * @param array $data data to import the course with. * @return tool_uploadcourse_course */ protected function get_course($data) { $importoptions = array( 'candelete' => $this->allowdeletes, 'canrename' => $this->allowrenames, 'canreset' => $this->allowresets, 'reset' => $this->reset, 'restoredir' => $this->get_restore_content_dir(), 'shortnametemplate' => $this->shortnametemplate ); return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions); } /** * Return the errors. * * @return array */ public function get_errors() { return $this->errors; } /** * Get the directory of the object to restore. * * @return string subdirectory in $CFG->backuptempdir/... */ protected function get_restore_content_dir() { $backupfile = null; $shortname = null; if (!empty($this->restorefile)) { $backupfile = $this->restorefile; } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) { $shortname = $this->templatecourse; } $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname); return $dir; } /** * Log errors on the current line. * * @param array $errors array of errors * @return void */ protected function log_error($errors) { if (empty($errors)) { return; } foreach ($errors as $code => $langstring) { if (!isset($this->errors[$this->linenb])) { $this->errors[$this->linenb] = array(); } $this->errors[$this->linenb][$code] = $langstring; } } /** * Parse a line to return an array(column => value) * * @param array $line returned by csv_import_reader * @return array */ protected function parse_line($line) { $data = array(); foreach ($line as $keynum => $value) { if (!isset($this->columns[$keynum])) { // This should not happen. continue; } $key = $this->columns[$keynum]; $data[$key] = $value; } return $data; } /** * Return a preview of the import. * * This only returns passed data, along with the errors. * * @param integer $rows number of rows to preview. * @param object $tracker the output tracker to use. * @return array of preview data. */ public function preview($rows = 10, $tracker = null) { if ($this->processstarted) { throw new coding_exception('Process has already been started'); } $this->processstarted = true; if (empty($tracker)) { $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT); } $tracker->start(); // We might need extra time and memory depending on the number of rows to preview. core_php_time_limit::raise(); raise_memory_limit(MEMORY_EXTRA); // Loop over the CSV lines. $preview = array(); while (($line = $this->cir->next()) && $rows > $this->linenb) { $this->linenb++; $data = $this->parse_line($line); $course = $this->get_course($data); $result = $course->prepare(); if (!$result) { $tracker->output($this->linenb, $result, $course->get_errors(), $data); } else { $tracker->output($this->linenb, $result, $course->get_statuses(), $data); } $row = $data; $preview[$this->linenb] = $row; } $tracker->finish(); return $preview; } /** * Reset the current process. * * @return void. */ public function reset() { $this->processstarted = false; $this->linenb = 0; $this->cir->init(); $this->errors = array(); } /** * Validation. * * @return void */ protected function validate() { if (empty($this->columns)) { throw new moodle_exception('cannotreadtmpfile', 'error'); } else if (count($this->columns) < 2) { throw new moodle_exception('csvfewcolumns', 'error'); } } } tracker.php 0000644 00000017236 15152024723 0006722 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Output tracker. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/weblib.php'); /** * Class output tracker. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_uploadcourse_tracker { /** * Constant to output nothing. */ const NO_OUTPUT = 0; /** * Constant to output HTML. */ const OUTPUT_HTML = 1; /** * Constant to output plain text. */ const OUTPUT_PLAIN = 2; /** * @var array columns to display. */ protected $columns = array('line', 'result', 'id', 'shortname', 'fullname', 'idnumber', 'status'); /** * @var int row number. */ protected $rownb = 0; /** * @var int chosen output mode. */ protected $outputmode; /** * @var object output buffer. */ protected $buffer; /** * Constructor. * * @param int $outputmode desired output mode. */ public function __construct($outputmode = self::NO_OUTPUT) { $this->outputmode = $outputmode; if ($this->outputmode == self::OUTPUT_PLAIN) { $this->buffer = new progress_trace_buffer(new text_progress_trace()); } } /** * Finish the output. * * @return void */ public function finish() { if ($this->outputmode == self::NO_OUTPUT) { return; } if ($this->outputmode == self::OUTPUT_HTML) { echo html_writer::end_tag('table'); } } /** * Output the results. * * @param int $total total courses. * @param int $created count of courses created. * @param int $updated count of courses updated. * @param int $deleted count of courses deleted. * @param int $errors count of errors. * @return void */ public function results($total, $created, $updated, $deleted, $errors) { if ($this->outputmode == self::NO_OUTPUT) { return; } $message = array( get_string('coursestotal', 'tool_uploadcourse', $total), get_string('coursescreated', 'tool_uploadcourse', $created), get_string('coursesupdated', 'tool_uploadcourse', $updated), get_string('coursesdeleted', 'tool_uploadcourse', $deleted), get_string('courseserrors', 'tool_uploadcourse', $errors) ); if ($this->outputmode == self::OUTPUT_PLAIN) { foreach ($message as $msg) { $this->buffer->output($msg); } } else if ($this->outputmode == self::OUTPUT_HTML) { $buffer = new progress_trace_buffer(new html_list_progress_trace()); foreach ($message as $msg) { $buffer->output($msg); } $buffer->finished(); } } /** * Output one more line. * * @param int $line line number. * @param bool $outcome success or not? * @param array $status array of statuses. * @param array $data extra data to display. * @return void */ public function output($line, $outcome, $status, $data) { global $OUTPUT; if ($this->outputmode == self::NO_OUTPUT) { return; } if ($this->outputmode == self::OUTPUT_PLAIN) { $message = array( $line, $outcome ? 'OK' : 'NOK', isset($data['id']) ? $data['id'] : '', isset($data['shortname']) ? $data['shortname'] : '', isset($data['fullname']) ? $data['fullname'] : '', isset($data['idnumber']) ? $data['idnumber'] : '' ); $this->buffer->output(implode("\t", $message)); if (!empty($status)) { foreach ($status as $st) { $this->buffer->output($st, 1); } } } else if ($this->outputmode == self::OUTPUT_HTML) { $ci = 0; $this->rownb++; if (is_array($status)) { $status = implode(html_writer::empty_tag('br'), $status); } if ($outcome) { $outcome = $OUTPUT->pix_icon('i/valid', ''); } else { $outcome = $OUTPUT->pix_icon('i/invalid', ''); } echo html_writer::start_tag('tr', array('class' => 'r' . $this->rownb % 2)); echo html_writer::tag('td', $line, array('class' => 'c' . $ci++)); echo html_writer::tag('td', $outcome, array('class' => 'c' . $ci++)); echo html_writer::tag('td', isset($data['id']) ? $data['id'] : '', array('class' => 'c' . $ci++)); echo html_writer::tag('td', isset($data['shortname']) ? $data['shortname'] : '', array('class' => 'c' . $ci++)); echo html_writer::tag('td', isset($data['fullname']) ? $data['fullname'] : '', array('class' => 'c' . $ci++)); echo html_writer::tag('td', isset($data['idnumber']) ? $data['idnumber'] : '', array('class' => 'c' . $ci++)); echo html_writer::tag('td', $status, array('class' => 'c' . $ci++)); echo html_writer::end_tag('tr'); } } /** * Start the output. * * @return void */ public function start() { if ($this->outputmode == self::NO_OUTPUT) { return; } if ($this->outputmode == self::OUTPUT_PLAIN) { $columns = array_flip($this->columns); unset($columns['status']); $columns = array_flip($columns); $this->buffer->output(implode("\t", $columns)); } else if ($this->outputmode == self::OUTPUT_HTML) { $ci = 0; echo html_writer::start_tag('table', array('class' => 'generaltable boxaligncenter flexible-wrap', 'summary' => get_string('uploadcoursesresult', 'tool_uploadcourse'))); echo html_writer::start_tag('tr', array('class' => 'heading r' . $this->rownb)); echo html_writer::tag('th', get_string('csvline', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col')); echo html_writer::tag('th', get_string('result', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col')); echo html_writer::tag('th', get_string('id', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col')); echo html_writer::tag('th', get_string('shortname'), array('class' => 'c' . $ci++, 'scope' => 'col')); echo html_writer::tag('th', get_string('fullname'), array('class' => 'c' . $ci++, 'scope' => 'col')); echo html_writer::tag('th', get_string('idnumber'), array('class' => 'c' . $ci++, 'scope' => 'col')); echo html_writer::tag('th', get_string('status'), array('class' => 'c' . $ci++, 'scope' => 'col')); echo html_writer::end_tag('tr'); } } } course.php 0000644 00000140130 15152024723 0006555 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * File containing the course class. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->dirroot . '/course/lib.php'); /** * Course class. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_uploadcourse_course { /** Outcome of the process: creating the course */ const DO_CREATE = 1; /** Outcome of the process: updating the course */ const DO_UPDATE = 2; /** Outcome of the process: deleting the course */ const DO_DELETE = 3; /** @var array assignable roles. */ protected $assignableroles = []; /** @var array Roles context levels. */ protected $contextlevels = []; /** @var array final import data. */ protected $data = array(); /** @var array default values. */ protected $defaults = array(); /** @var array enrolment data. */ protected $enrolmentdata; /** @var array errors. */ protected $errors = array(); /** @var int the ID of the course that had been processed. */ protected $id; /** @var array containing options passed from the processor. */ protected $importoptions = array(); /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */ protected $mode; /** @var array course import options. */ protected $options = array(); /** @var int constant value of self::DO_*, what to do with that course */ protected $do; /** @var bool set to true once we have prepared the course */ protected $prepared = false; /** @var bool set to true once we have started the process of the course */ protected $processstarted = false; /** @var array course import data. */ protected $rawdata = array(); /** @var array restore directory. */ protected $restoredata; /** @var string course shortname. */ protected $shortname; /** @var array errors. */ protected $statuses = array(); /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */ protected $updatemode; /** @var array fields allowed as course data. */ static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', 'enddate', 'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes', 'groupmode', 'groupmodeforce', 'enablecompletion', 'downloadcontent'); /** @var array fields required on course creation. */ static protected $mandatoryfields = array('fullname', 'category'); /** @var array fields which are considered as options. */ static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null, 'templatecourse' => null, 'reset' => false); /** @var array options determining what can or cannot be done at an import level. */ static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false, 'reset' => false, 'restoredir' => null, 'shortnametemplate' => null); /** * Constructor * * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_* * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_* * @param array $rawdata raw course data. * @param array $defaults default course data. * @param array $importoptions import options. */ public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) { if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL && $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE && $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) { throw new coding_exception('Incorrect mode.'); } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING && $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY && $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS && $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) { throw new coding_exception('Incorrect update mode.'); } $this->mode = $mode; $this->updatemode = $updatemode; if (isset($rawdata['shortname'])) { $this->shortname = $rawdata['shortname']; } $this->rawdata = $rawdata; $this->defaults = $defaults; // Extract course options. foreach (self::$optionfields as $option => $default) { $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default; } // Import options. foreach (self::$importoptionsdefaults as $option => $default) { $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default; } } /** * Does the mode allow for course creation? * * @return bool */ public function can_create() { return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, tool_uploadcourse_processor::MODE_CREATE_NEW, tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE) ); } /** * Does the mode allow for course deletion? * * @return bool */ public function can_delete() { return $this->importoptions['candelete']; } /** * Does the mode only allow for course creation? * * @return bool */ public function can_only_create() { return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, tool_uploadcourse_processor::MODE_CREATE_NEW)); } /** * Does the mode allow for course rename? * * @return bool */ public function can_rename() { return $this->importoptions['canrename']; } /** * Does the mode allow for course reset? * * @return bool */ public function can_reset() { return $this->importoptions['canreset']; } /** * Does the mode allow for course update? * * @return bool */ public function can_update() { return in_array($this->mode, array( tool_uploadcourse_processor::MODE_UPDATE_ONLY, tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE) ) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING; } /** * Can we use default values? * * @return bool */ public function can_use_defaults() { return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS, tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS)); } /** * Delete the current course. * * @return bool */ protected function delete() { global $DB; $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname', array('shortname' => $this->shortname), MUST_EXIST); return delete_course($this->id, false); } /** * Log an error * * @param string $code error code. * @param lang_string $message error message. * @return void */ protected function error($code, lang_string $message) { if (array_key_exists($code, $this->errors)) { throw new coding_exception('Error code already defined'); } $this->errors[$code] = $message; } /** * Return whether the course exists or not. * * @param string $shortname the shortname to use to check if the course exists. Falls back on $this->shortname if empty. * @return bool */ protected function exists($shortname = null) { global $DB; if (is_null($shortname)) { $shortname = $this->shortname; } if (!empty($shortname) || is_numeric($shortname)) { return $DB->record_exists('course', array('shortname' => $shortname)); } return false; } /** * Return the data that will be used upon saving. * * @return null|array */ public function get_data() { return $this->data; } /** * Return the errors found during preparation. * * @return array */ public function get_errors() { return $this->errors; } /** * Return array of valid fields for default values * * @return array */ protected function get_valid_fields() { return array_merge(self::$validfields, \tool_uploadcourse_helper::get_custom_course_field_names()); } /** * Assemble the course data based on defaults. * * This returns the final data to be passed to create_course(). * * @param array $data current data. * @return array */ protected function get_final_create_data($data) { foreach ($this->get_valid_fields() as $field) { if (!isset($data[$field]) && isset($this->defaults[$field])) { $data[$field] = $this->defaults[$field]; } } $data['shortname'] = $this->shortname; return $data; } /** * Assemble the course data based on defaults. * * This returns the final data to be passed to update_course(). * * @param array $data current data. * @param bool $usedefaults are defaults allowed? * @param bool $missingonly ignore fields which are already set. * @return array */ protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) { global $DB; $newdata = array(); $existingdata = $DB->get_record('course', array('shortname' => $this->shortname)); foreach ($this->get_valid_fields() as $field) { if ($missingonly) { if (isset($existingdata->$field) and $existingdata->$field !== '') { continue; } } if (isset($data[$field])) { $newdata[$field] = $data[$field]; } else if ($usedefaults && isset($this->defaults[$field])) { $newdata[$field] = $this->defaults[$field]; } } $newdata['id'] = $existingdata->id; return $newdata; } /** * Return the ID of the processed course. * * @return int|null */ public function get_id() { if (!$this->processstarted) { throw new coding_exception('The course has not been processed yet!'); } return $this->id; } /** * Get the directory of the object to restore. * * @return string|false|null subdirectory in $CFG->backuptempdir/..., false when an error occured * and null when there is simply nothing. */ protected function get_restore_content_dir() { $backupfile = null; $shortname = null; if (!empty($this->options['backupfile'])) { $backupfile = $this->options['backupfile']; } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) { $shortname = $this->options['templatecourse']; } $errors = array(); $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname, $errors); if (!empty($errors)) { foreach ($errors as $key => $message) { $this->error($key, $message); } return false; } else if ($dir === false) { // We want to return null when nothing was wrong, but nothing was found. $dir = null; } if (empty($dir) && !empty($this->importoptions['restoredir'])) { $dir = $this->importoptions['restoredir']; } return $dir; } /** * Return the errors found during preparation. * * @return array */ public function get_statuses() { return $this->statuses; } /** * Return whether there were errors with this course. * * @return boolean */ public function has_errors() { return !empty($this->errors); } /** * Validates and prepares the data. * * @return bool false is any error occured. */ public function prepare() { global $DB, $SITE, $CFG; $this->prepared = true; // Validate the shortname. if (!empty($this->shortname) || is_numeric($this->shortname)) { if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) { $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); return false; } // Ensure we don't overflow the maximum length of the shortname field. if (core_text::strlen($this->shortname) > 255) { $this->error('invalidshortnametoolong', new lang_string('invalidshortnametoolong', 'tool_uploadcourse', 255)); return false; } } $exists = $this->exists(); // Do we want to delete the course? if ($this->options['delete']) { if (!$exists) { $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse')); return false; } else if (!$this->can_delete()) { $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse')); return false; } $this->do = self::DO_DELETE; return true; } // Can we create/update the course under those conditions? if ($exists) { if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { $this->error('courseexistsanduploadnotallowed', new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); return false; } else if ($this->can_update()) { // We can never allow for any front page changes! if ($this->shortname == $SITE->shortname) { $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse')); return false; } } } else { if (!$this->can_create()) { $this->error('coursedoesnotexistandcreatenotallowed', new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse')); return false; } } // Basic data. $coursedata = array(); foreach ($this->rawdata as $field => $value) { if (!in_array($field, self::$validfields)) { continue; } else if ($field == 'shortname') { // Let's leave it apart from now, use $this->shortname only. continue; } $coursedata[$field] = $value; } $mode = $this->mode; $updatemode = $this->updatemode; $usedefaults = $this->can_use_defaults(); // Resolve the category, and fail if not found. $errors = array(); $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors); if (empty($errors)) { $coursedata['category'] = $catid; } else { foreach ($errors as $key => $message) { $this->error($key, $message); } return false; } // Ensure we don't overflow the maximum length of the fullname field. if (!empty($coursedata['fullname']) && core_text::strlen($coursedata['fullname']) > 254) { $this->error('invalidfullnametoolong', new lang_string('invalidfullnametoolong', 'tool_uploadcourse', 254)); return false; } // If the course does not exist, or will be forced created. if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { // Mandatory fields upon creation. $errors = array(); foreach (self::$mandatoryfields as $field) { if ((!isset($coursedata[$field]) || $coursedata[$field] === '') && (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) { $errors[] = $field; } } if (!empty($errors)) { $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse', implode(', ', $errors))); return false; } } // Should the course be renamed? if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) { if (!$this->can_update()) { $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse')); return false; } else if (!$exists) { $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse')); return false; } else if (!$this->can_rename()) { $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse')); return false; } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) { $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); return false; } else if ($this->exists($this->options['rename'])) { $this->error('cannotrenameshortnamealreadyinuse', new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse')); return false; } else if (isset($coursedata['idnumber']) && $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn', array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) { $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse')); return false; } $coursedata['shortname'] = $this->options['rename']; $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse', array('from' => $this->shortname, 'to' => $coursedata['shortname']))); } // Should we generate a shortname? if (empty($this->shortname) && !is_numeric($this->shortname)) { if (empty($this->importoptions['shortnametemplate'])) { $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse')); return false; } else if (!$this->can_only_create()) { $this->error('cannotgenerateshortnameupdatemode', new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse')); return false; } else { $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata, $this->importoptions['shortnametemplate']); if (is_null($newshortname)) { $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse')); return false; } else if ($this->exists($newshortname)) { if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { $this->error('generatedshortnamealreadyinuse', new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse')); return false; } $exists = true; } $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse', $newshortname)); $this->shortname = $newshortname; } } // If exists, but we only want to create courses, increment the shortname. if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { $original = $this->shortname; $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname); $exists = false; if ($this->shortname != $original) { $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse', array('from' => $original, 'to' => $this->shortname))); if (isset($coursedata['idnumber'])) { $originalidn = $coursedata['idnumber']; $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']); if ($originalidn != $coursedata['idnumber']) { $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse', array('from' => $originalidn, 'to' => $coursedata['idnumber']))); } } } } // If the course does not exist, ensure that the ID number is not taken. if (!$exists && isset($coursedata['idnumber'])) { if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) { $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse')); return false; } } // Course start date. if (!empty($coursedata['startdate'])) { $coursedata['startdate'] = strtotime($coursedata['startdate']); } // Course end date. if (!empty($coursedata['enddate'])) { $coursedata['enddate'] = strtotime($coursedata['enddate']); } // If lang is specified, check the user is allowed to set that field. if (!empty($coursedata['lang'])) { if ($exists) { $courseid = $DB->get_field('course', 'id', ['shortname' => $this->shortname]); if (!has_capability('moodle/course:setforcedlanguage', context_course::instance($courseid))) { $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse')); return false; } } else { $catcontext = context_coursecat::instance($coursedata['category']); if (!guess_if_creator_will_have_course_capability('moodle/course:setforcedlanguage', $catcontext)) { $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse')); return false; } } } // Ultimate check mode vs. existence. switch ($mode) { case tool_uploadcourse_processor::MODE_CREATE_NEW: case tool_uploadcourse_processor::MODE_CREATE_ALL: if ($exists) { $this->error('courseexistsanduploadnotallowed', new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); return false; } break; case tool_uploadcourse_processor::MODE_UPDATE_ONLY: if (!$exists) { $this->error('coursedoesnotexistandcreatenotallowed', new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse')); return false; } // No break! case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE: if ($exists) { if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) { $this->error('updatemodedoessettonothing', new lang_string('updatemodedoessettonothing', 'tool_uploadcourse')); return false; } } break; default: // O_o Huh?! This should really never happen here! $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse')); return false; } // Get final data. if ($exists) { $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS); $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly); // Make sure we are not trying to mess with the front page, though we should never get here! if ($coursedata['id'] == $SITE->id) { $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse')); return false; } $this->do = self::DO_UPDATE; } else { $coursedata = $this->get_final_create_data($coursedata); $this->do = self::DO_CREATE; } // Validate course start and end dates. if ($exists) { // We also check existing start and end dates if we are updating an existing course. $existingdata = $DB->get_record('course', array('shortname' => $this->shortname)); if (empty($coursedata['startdate'])) { $coursedata['startdate'] = $existingdata->startdate; } if (empty($coursedata['enddate'])) { $coursedata['enddate'] = $existingdata->enddate; } } if ($errorcode = course_validate_dates($coursedata)) { $this->error($errorcode, new lang_string($errorcode, 'error')); return false; } // Add role renaming. $errors = array(); $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors); if (!empty($errors)) { foreach ($errors as $key => $message) { $this->error($key, $message); } return false; } foreach ($rolenames as $rolekey => $rolename) { $coursedata[$rolekey] = $rolename; } // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context. if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) { $context = context_course::instance($coursedata['id']); } else { // The category ID is taken from the defaults if it exists, otherwise from course data. $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']); } $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context, $errors); if (!empty($errors)) { foreach ($errors as $key => $message) { $this->error($key, $message); } return false; } foreach ($customfielddata as $name => $value) { $coursedata[$name] = $value; } // Some validation. if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) { $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse')); return false; } // Add data for course format options. if (isset($coursedata['format']) || $exists) { if (isset($coursedata['format'])) { $courseformat = course_get_format((object)['format' => $coursedata['format']]); } else { $courseformat = course_get_format($existingdata); } $coursedata += $courseformat->validate_course_format_options($this->rawdata); } // Special case, 'numsections' is not a course format option any more but still should apply from the template course, // if any, and otherwise from defaults. if (!$exists || !array_key_exists('numsections', $coursedata)) { if (isset($this->rawdata['numsections']) && is_numeric($this->rawdata['numsections'])) { $coursedata['numsections'] = (int)$this->rawdata['numsections']; } else if (isset($this->options['templatecourse'])) { $numsections = tool_uploadcourse_helper::get_coursesection_count($this->options['templatecourse']); if ($numsections != 0) { $coursedata['numsections'] = $numsections; } else { $coursedata['numsections'] = get_config('moodlecourse', 'numsections'); } } else { $coursedata['numsections'] = get_config('moodlecourse', 'numsections'); } } // Visibility can only be 0 or 1. if (!empty($coursedata['visible']) AND !($coursedata['visible'] == 0 OR $coursedata['visible'] == 1)) { $this->error('invalidvisibilitymode', new lang_string('invalidvisibilitymode', 'tool_uploadcourse')); return false; } // Ensure that user is allowed to configure course content download and the field contains a valid value. if (isset($coursedata['downloadcontent'])) { if (!$CFG->downloadcoursecontentallowed || !has_capability('moodle/course:configuredownloadcontent', $context)) { $this->error('downloadcontentnotallowed', new lang_string('downloadcontentnotallowed', 'tool_uploadcourse')); return false; } $downloadcontentvalues = [ DOWNLOAD_COURSE_CONTENT_DISABLED, DOWNLOAD_COURSE_CONTENT_ENABLED, DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT, ]; if (!in_array($coursedata['downloadcontent'], $downloadcontentvalues)) { $this->error('invaliddownloadcontent', new lang_string('invaliddownloadcontent', 'tool_uploadcourse')); return false; } } // Saving data. $this->data = $coursedata; // Get enrolment data. Where the course already exists, we can also perform validation. $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata); $courseid = $coursedata['id'] ?? 0; $errors = $this->validate_enrolment_data($courseid, $this->enrolmentdata); if (!empty($errors)) { foreach ($errors as $key => $message) { $this->error($key, $message); } return false; } if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') { $this->data['tags'] = preg_split('/\s*,\s*/', trim($this->rawdata['tags']), -1, PREG_SPLIT_NO_EMPTY); } // Restore data. // TODO Speed up things by not really extracting the backup just yet, but checking that // the backup file or shortname passed are valid. Extraction should happen in proceed(). $this->restoredata = $this->get_restore_content_dir(); if ($this->restoredata === false) { return false; } // We can only reset courses when allowed and we are updating the course. if ($this->importoptions['reset'] || $this->options['reset']) { if ($this->do !== self::DO_UPDATE) { $this->error('canonlyresetcourseinupdatemode', new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse')); return false; } else if (!$this->can_reset()) { $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse')); return false; } } return true; } /** * Proceed with the import of the course. * * @return void */ public function proceed() { global $CFG, $USER; if (!$this->prepared) { throw new coding_exception('The course has not been prepared.'); } else if ($this->has_errors()) { throw new moodle_exception('Cannot proceed, errors were detected.'); } else if ($this->processstarted) { throw new coding_exception('The process has already been started.'); } $this->processstarted = true; if ($this->do === self::DO_DELETE) { if ($this->delete()) { $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse')); } else { $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse')); } return true; } else if ($this->do === self::DO_CREATE) { $course = create_course((object) $this->data); $this->id = $course->id; $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse')); } else if ($this->do === self::DO_UPDATE) { $course = (object) $this->data; update_course($course); $this->id = $course->id; $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse')); } else { // Strangely the outcome has not been defined, or is unknown! throw new coding_exception('Unknown outcome!'); } // Restore a course. if (!empty($this->restoredata)) { $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); // Check if the format conversion must happen first. if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) { $rc->convert(); } if ($rc->execute_precheck()) { $rc->execute_plan(); $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse')); } else { $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse')); } $rc->destroy(); } // Proceed with enrolment data. $this->process_enrolment_data($course); // Reset the course. if ($this->importoptions['reset'] || $this->options['reset']) { if ($this->do === self::DO_UPDATE && $this->can_reset()) { $this->reset($course); $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse')); } } // Mark context as dirty. $context = context_course::instance($course->id); $context->mark_dirty(); } /** * Validate passed enrolment data against an existing course * * @param int $courseid * @param array[] $enrolmentdata * @return lang_string[] Errors keyed on error code */ protected function validate_enrolment_data(int $courseid, array $enrolmentdata): array { global $DB; // Nothing to validate. if (empty($enrolmentdata)) { return []; } $errors = []; $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins(); $instances = enrol_get_instances($courseid, false); foreach ($enrolmentdata as $method => $options) { if (isset($options['role'])) { $role = $options['role']; if ($courseid) { if (!$this->validate_role_context($courseid, $role)) { $errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role); break; } } else { // We can at least check that context level is correct while actual context not exist. $roleid = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST); if (!$this->validate_role_context_level($roleid)) { $errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role); break; } } } if ($courseid) { $plugin = $enrolmentplugins[$method]; // Find matching instances by enrolment method. $methodinstances = array_filter($instances, static function (stdClass $instance) use ($method) { return (strcmp($instance->enrol, $method) == 0); }); if (!empty($options['delete'])) { // Ensure user is able to delete the instances. foreach ($methodinstances as $methodinstance) { if (!$plugin->can_delete_instance($methodinstance)) { $errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse', $plugin->get_instance_name($methodinstance)); break; } } } else if (!empty($options['disable'])) { // Ensure user is able to toggle instance statuses. foreach ($methodinstances as $methodinstance) { if (!$plugin->can_hide_show_instance($methodinstance)) { $errors['errorcannotdisableenrolment'] = new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse', $plugin->get_instance_name($methodinstance)); break; } } } else { // Ensure user is able to create/update instance. $methodinstance = empty($methodinstances) ? null : reset($methodinstances); if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) || (!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) { $errors['errorcannotcreateorupdateenrolment'] = new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse', $plugin->get_instance_name($methodinstance)); break; } } } } return $errors; } /** * Add the enrolment data for the course. * * @param object $course course record. * @return void */ protected function process_enrolment_data($course) { global $DB; $enrolmentdata = $this->enrolmentdata; if (empty($enrolmentdata)) { return; } $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins(); $instances = enrol_get_instances($course->id, false); foreach ($enrolmentdata as $enrolmethod => $method) { $instance = null; foreach ($instances as $i) { if ($i->enrol == $enrolmethod) { $instance = $i; break; } } $todelete = isset($method['delete']) && $method['delete']; $todisable = isset($method['disable']) && $method['disable']; unset($method['delete']); unset($method['disable']); if ($todelete) { // Remove the enrolment method. if ($instance) { $plugin = $enrolmentplugins[$instance->enrol]; // Ensure user is able to delete the instance. if ($plugin->can_delete_instance($instance)) { $plugin->delete_instance($instance); } else { $this->error('errorcannotdeleteenrolment', new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse', $plugin->get_instance_name($instance))); } } } else { // Create/update enrolment. $plugin = $enrolmentplugins[$enrolmethod]; $status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED; // Create a new instance if necessary. if (empty($instance) && $plugin->can_add_instance($course->id)) { $instanceid = $plugin->add_default_instance($course); $instance = $DB->get_record('enrol', ['id' => $instanceid]); $instance->roleid = $plugin->get_config('roleid'); // On creation the user can decide the status. $plugin->update_status($instance, $status); } // Check if the we need to update the instance status. if ($instance && $status != $instance->status) { if ($plugin->can_hide_show_instance($instance)) { $plugin->update_status($instance, $status); } else { $this->error('errorcannotdisableenrolment', new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse', $plugin->get_instance_name($instance))); break; } } if (empty($instance) || !$plugin->can_edit_instance($instance)) { $this->error('errorcannotcreateorupdateenrolment', new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse', $plugin->get_instance_name($instance))); break; } // Now update values. foreach ($method as $k => $v) { $instance->{$k} = $v; } // Sort out the start, end and date. $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0); $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0); // Is the enrolment period set? if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) { if (preg_match('/^\d+$/', $method['enrolperiod'])) { $method['enrolperiod'] = (int) $method['enrolperiod']; } else { // Try and convert period to seconds. $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']); } $instance->enrolperiod = $method['enrolperiod']; } if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) { $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod']; } if ($instance->enrolenddate > 0) { $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate; } if ($instance->enrolenddate < $instance->enrolstartdate) { $instance->enrolenddate = $instance->enrolstartdate; } // Sort out the given role. if (isset($method['role'])) { $role = $method['role']; if (!$this->validate_role_context($course->id, $role)) { $this->error('contextrolenotallowed', new lang_string('contextrolenotallowed', 'core_role', $role)); break; } $roleids = tool_uploadcourse_helper::get_role_ids(); if (isset($roleids[$method['role']])) { $instance->roleid = $roleids[$method['role']]; } } $instance->timemodified = time(); $DB->update_record('enrol', $instance); } } } /** * Check if role is allowed in course context * * @param int $courseid course context. * @param string $role Role. * @return bool */ protected function validate_role_context(int $courseid, string $role) : bool { if (empty($this->assignableroles[$courseid])) { $coursecontext = \context_course::instance($courseid); $this->assignableroles[$courseid] = get_assignable_roles($coursecontext, ROLENAME_SHORT); } if (!in_array($role, $this->assignableroles[$courseid])) { return false; } return true; } /** * Check if role is allowed at this context level. * * @param int $roleid Role ID. * @return bool */ protected function validate_role_context_level(int $roleid) : bool { if (empty($this->contextlevels[$roleid])) { $this->contextlevels[$roleid] = get_role_contextlevels($roleid); } if (!in_array(CONTEXT_COURSE, $this->contextlevels[$roleid])) { return false; } return true; } /** * Reset the current course. * * This does not reset any of the content of the activities. * * @param stdClass $course the course object of the course to reset. * @return array status array of array component, item, error. */ protected function reset($course) { global $DB; $resetdata = new stdClass(); $resetdata->id = $course->id; $resetdata->reset_start_date = time(); $resetdata->reset_events = true; $resetdata->reset_notes = true; $resetdata->delete_blog_associations = true; $resetdata->reset_completion = true; $resetdata->reset_roles_overrides = true; $resetdata->reset_roles_local = true; $resetdata->reset_groups_members = true; $resetdata->reset_groups_remove = true; $resetdata->reset_groupings_members = true; $resetdata->reset_groupings_remove = true; $resetdata->reset_gradebook_items = true; $resetdata->reset_gradebook_grades = true; $resetdata->reset_comments = true; if (empty($course->startdate)) { $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id)); } $resetdata->reset_start_date_old = $course->startdate; if (empty($course->enddate)) { $course->enddate = $DB->get_field_select('course', 'enddate', 'id = :id', array('id' => $course->id)); } $resetdata->reset_end_date_old = $course->enddate; // Add roles. $roles = tool_uploadcourse_helper::get_role_ids(); $resetdata->unenrol_users = array_values($roles); $resetdata->unenrol_users[] = 0; // Enrolled without role. return reset_course_userdata($resetdata); } /** * Log a status * * @param string $code status code. * @param lang_string $message status message. * @return void */ protected function status($code, lang_string $message) { if (array_key_exists($code, $this->statuses)) { throw new coding_exception('Status code already defined'); } $this->statuses[$code] = $message; } } base_form.php 0000644 00000011472 15152024723 0007220 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * File containing the base import form. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); /** * Base import form. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_uploadcourse_base_form extends moodleform { /** * Empty definition. * * @return void */ public function definition() { } /** * Adds the import settings part. * * @return void */ public function add_import_options() { $mform = $this->_form; // Upload settings and file. $mform->addElement('header', 'importoptionshdr', get_string('importoptions', 'tool_uploadcourse')); $mform->setExpanded('importoptionshdr', true); $choices = array( tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('createnew', 'tool_uploadcourse'), tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('createall', 'tool_uploadcourse'), tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('createorupdate', 'tool_uploadcourse'), tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('updateonly', 'tool_uploadcourse') ); $mform->addElement('select', 'options[mode]', get_string('mode', 'tool_uploadcourse'), $choices); $mform->addHelpButton('options[mode]', 'mode', 'tool_uploadcourse'); $choices = array( tool_uploadcourse_processor::UPDATE_NOTHING => get_string('nochanges', 'tool_uploadcourse'), tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY => get_string('updatewithdataonly', 'tool_uploadcourse'), tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS => get_string('updatewithdataordefaults', 'tool_uploadcourse'), tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('updatemissing', 'tool_uploadcourse') ); $mform->addElement('select', 'options[updatemode]', get_string('updatemode', 'tool_uploadcourse'), $choices); $mform->setDefault('options[updatemode]', tool_uploadcourse_processor::UPDATE_NOTHING); $mform->hideIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); $mform->hideIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); $mform->addHelpButton('options[updatemode]', 'updatemode', 'tool_uploadcourse'); $mform->addElement('selectyesno', 'options[allowdeletes]', get_string('allowdeletes', 'tool_uploadcourse')); $mform->setDefault('options[allowdeletes]', 0); $mform->hideIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); $mform->hideIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); $mform->addHelpButton('options[allowdeletes]', 'allowdeletes', 'tool_uploadcourse'); $mform->addElement('selectyesno', 'options[allowrenames]', get_string('allowrenames', 'tool_uploadcourse')); $mform->setDefault('options[allowrenames]', 0); $mform->hideIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); $mform->hideIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); $mform->addHelpButton('options[allowrenames]', 'allowrenames', 'tool_uploadcourse'); $mform->addElement('selectyesno', 'options[allowresets]', get_string('allowresets', 'tool_uploadcourse')); $mform->setDefault('options[allowresets]', 0); $mform->hideIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); $mform->hideIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); $mform->addHelpButton('options[allowresets]', 'allowresets', 'tool_uploadcourse'); } } step2_form.php 0000644 00000031475 15152024723 0007350 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Bulk course upload step 2. * * @package tool_uploadcourse * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/course/lib.php'); /** * Specify course upload details. * * @package tool_uploadcourse * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { /** * The standard form definiton. * @return void. */ public function definition () { global $CFG; $mform = $this->_form; $data = $this->_customdata['data']; $courseconfig = get_config('moodlecourse'); // Import options. $this->add_import_options(); // Course options. $mform->addElement('header', 'courseoptionshdr', get_string('courseprocess', 'tool_uploadcourse')); $mform->setExpanded('courseoptionshdr', true); $mform->addElement('text', 'options[shortnametemplate]', get_string('shortnametemplate', 'tool_uploadcourse'), 'maxlength="100" size="20"'); $mform->setType('options[shortnametemplate]', PARAM_RAW); $mform->addHelpButton('options[shortnametemplate]', 'shortnametemplate', 'tool_uploadcourse'); $mform->hideIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE); $mform->hideIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY); // Restore file is not in the array options on purpose, because formslib can't handle it! $contextid = $this->_customdata['contextid']; $mform->addElement('hidden', 'contextid', $contextid); $mform->setType('contextid', PARAM_INT); $mform->addElement('filepicker', 'restorefile', get_string('templatefile', 'tool_uploadcourse')); $mform->addHelpButton('restorefile', 'templatefile', 'tool_uploadcourse'); $mform->addElement('text', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse')); $mform->setType('options[templatecourse]', PARAM_TEXT); $mform->addHelpButton('options[templatecourse]', 'coursetemplatename', 'tool_uploadcourse'); $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse')); $mform->setDefault('options[reset]', 0); $mform->hideIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); $mform->hideIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); $mform->disabledIf('options[reset]', 'options[allowresets]', 'eq', 0); $mform->addHelpButton('options[reset]', 'reset', 'tool_uploadcourse'); // Default values. $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse')); $mform->setExpanded('defaultheader', true); $displaylist = core_course_category::make_categories_list('moodle/course:create'); $mform->addElement('autocomplete', 'defaults[category]', get_string('coursecategory'), $displaylist); $mform->addRule('defaults[category]', null, 'required', null, 'client'); $mform->addHelpButton('defaults[category]', 'coursecategory'); $choices = array(); $choices['0'] = get_string('hide'); $choices['1'] = get_string('show'); $mform->addElement('select', 'defaults[visible]', get_string('coursevisibility'), $choices); $mform->addHelpButton('defaults[visible]', 'coursevisibility'); $mform->setDefault('defaults[visible]', $courseconfig->visible); if ($CFG->downloadcoursecontentallowed && has_capability('moodle/course:configuredownloadcontent', context::instance_by_id($contextid))) { $downloadchoices = [ DOWNLOAD_COURSE_CONTENT_DISABLED => get_string('no'), DOWNLOAD_COURSE_CONTENT_ENABLED => get_string('yes'), ]; $sitedefaultstring = $downloadchoices[$courseconfig->downloadcontentsitedefault]; $downloadchoices[DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT] = get_string('sitedefaultspecified', '', $sitedefaultstring); $downloadselectdefault = $courseconfig->downloadcontent ?? DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT; $mform->addElement('select', 'defaults[downloadcontent]', get_string('enabledownloadcoursecontent', 'course'), $downloadchoices); $mform->addHelpButton('defaults[downloadcontent]', 'downloadcoursecontent', 'course'); $mform->setDefault('defaults[downloadcontent]', $downloadselectdefault); } $mform->addElement('date_time_selector', 'defaults[startdate]', get_string('startdate')); $mform->addHelpButton('defaults[startdate]', 'startdate'); $mform->setDefault('defaults[startdate]', time() + 3600 * 24); $mform->addElement('date_time_selector', 'defaults[enddate]', get_string('enddate'), array('optional' => true)); $mform->addHelpButton('defaults[enddate]', 'enddate'); $courseformats = get_sorted_course_formats(true); $formcourseformats = array(); foreach ($courseformats as $courseformat) { $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat"); } $mform->addElement('select', 'defaults[format]', get_string('format'), $formcourseformats); $mform->addHelpButton('defaults[format]', 'format'); $mform->setDefault('defaults[format]', $courseconfig->format); if (!empty($CFG->allowcoursethemes)) { $themeobjects = get_list_of_themes(); $themes=array(); $themes[''] = get_string('forceno'); foreach ($themeobjects as $key => $theme) { if (empty($theme->hidefromselector)) { $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); } } $mform->addElement('select', 'defaults[theme]', get_string('forcetheme'), $themes); } $languages = array(); $languages[''] = get_string('forceno'); $languages += get_string_manager()->get_list_of_translations(); $mform->addElement('select', 'defaults[lang]', get_string('forcelanguage'), $languages); $mform->setDefault('defaults[lang]', $courseconfig->lang); $options = range(0, 10); $mform->addElement('select', 'defaults[newsitems]', get_string('newsitemsnumber'), $options); $mform->addHelpButton('defaults[newsitems]', 'newsitemsnumber'); $mform->setDefault('defaults[newsitems]', $courseconfig->newsitems); $mform->addElement('selectyesno', 'defaults[showgrades]', get_string('showgrades')); $mform->addHelpButton('defaults[showgrades]', 'showgrades'); $mform->setDefault('defaults[showgrades]', $courseconfig->showgrades); $mform->addElement('selectyesno', 'defaults[showreports]', get_string('showreports')); $mform->addHelpButton('defaults[showreports]', 'showreports'); $mform->setDefault('defaults[showreports]', $courseconfig->showreports); if (!empty($CFG->legacyfilesinnewcourses)) { $mform->addElement('select', 'defaults[legacyfiles]', get_string('courselegacyfiles'), $choices); $mform->addHelpButton('defaults[legacyfiles]', 'courselegacyfiles'); if (!isset($courseconfig->legacyfiles)) { $courseconfig->legacyfiles = 0; } $mform->setDefault('defaults[legacyfiles]', $courseconfig->legacyfiles); } $choices = get_max_upload_sizes($CFG->maxbytes); $mform->addElement('select', 'defaults[maxbytes]', get_string('maximumupload'), $choices); $mform->addHelpButton('defaults[maxbytes]', 'maximumupload'); $mform->setDefault('defaults[maxbytes]', $courseconfig->maxbytes); $choices = array(); $choices[NOGROUPS] = get_string('groupsnone', 'group'); $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group'); $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group'); $mform->addElement('select', 'defaults[groupmode]', get_string('groupmode', 'group'), $choices); $mform->addHelpButton('defaults[groupmode]', 'groupmode', 'group'); $mform->setDefault('defaults[groupmode]', $courseconfig->groupmode); $mform->addElement('selectyesno', 'defaults[groupmodeforce]', get_string('groupmodeforce', 'group')); $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group'); $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce); // Completion tracking. if (!empty($CFG->enablecompletion)) { $mform->addElement('selectyesno', 'defaults[enablecompletion]', get_string('enablecompletion', 'completion')); $mform->setDefault('defaults[enablecompletion]', $courseconfig->enablecompletion); $mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion'); } // Add custom fields to the form. $handler = \core_course\customfield\course_handler::create(); $handler->instance_form_definition($mform, 0, 'defaultvaluescustomfieldcategory', 'tool_uploadcourse'); // Hidden fields. $mform->addElement('hidden', 'importid'); $mform->setType('importid', PARAM_INT); $mform->addElement('hidden', 'previewrows'); $mform->setType('previewrows', PARAM_INT); $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse')); // Prepare custom fields data. $data = (object) $data; $handler->instance_form_before_set_data($data); $this->set_data($data); } /** * Add actopm buttons. * * @param bool $cancel whether to show cancel button, default true * @param string $submitlabel label for submit button, defaults to get_string('savechanges') * @return void */ public function add_action_buttons($cancel = true, $submitlabel = null) { $mform =& $this->_form; $buttonarray = array(); $buttonarray[] = &$mform->createElement('submit', 'showpreview', get_string('preview', 'tool_uploadcourse')); $buttonarray[] = &$mform->createElement('submit', 'submitbutton', $submitlabel); $buttonarray[] = &$mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); $mform->closeHeaderBefore('buttonar'); } /** * Sets the enddate default after set_data is called. */ public function definition_after_data() { $mform = $this->_form; // The default end date depends on the course format. $format = course_get_format((object)array('format' => get_config('moodlecourse', 'format'))); // Check if course end date form field should be enabled by default. // If a default date is provided to the form element, it is magically enabled by default in the // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default. if (get_config('moodlecourse', 'courseenddateenabled')) { $enddate = $format->get_default_course_enddate($mform, array('startdate' => 'defaults[startdate]')); $mform->setDefault('defaults[enddate]', $enddate); } // Tweak the form with values provided by custom fields in use. \core_course\customfield\course_handler::create()->instance_form_definition_after_data($mform); } /** * Validation. * * @param array $data * @param array $files * @return array the errors that were found */ public function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); if ($errorcode = course_validate_dates($data['defaults'])) { $errors['defaults[enddate]'] = get_string($errorcode, 'error'); } // Custom fields validation. array_merge($errors, \core_course\customfield\course_handler::create()->instance_form_validation($data, $files)); return $errors; } } step1_form.php 0000644 00000006130 15152024723 0007335 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * File containing the step 1 of the upload form. * * @package tool_uploadcourse * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); /** * Upload a file CVS file with course information. * * @package tool_uploadcourse * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_uploadcourse_step1_form extends tool_uploadcourse_base_form { /** * The standard form definiton. * @return void */ public function definition () { $mform = $this->_form; $mform->addElement('header', 'generalhdr', get_string('general')); $mform->addElement('filepicker', 'coursefile', get_string('coursefile', 'tool_uploadcourse')); $mform->addRule('coursefile', null, 'required'); $mform->addHelpButton('coursefile', 'coursefile', 'tool_uploadcourse'); $choices = csv_import_reader::get_delimiter_list(); $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'tool_uploadcourse'), $choices); if (array_key_exists('cfg', $choices)) { $mform->setDefault('delimiter_name', 'cfg'); } else if (get_string('listsep', 'langconfig') == ';') { $mform->setDefault('delimiter_name', 'semicolon'); } else { $mform->setDefault('delimiter_name', 'comma'); } $mform->addHelpButton('delimiter_name', 'csvdelimiter', 'tool_uploadcourse'); $choices = core_text::get_encodings(); $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadcourse'), $choices); $mform->setDefault('encoding', 'UTF-8'); $mform->addHelpButton('encoding', 'encoding', 'tool_uploadcourse'); $choices = array('10' => 10, '20' => 20, '100' => 100, '1000' => 1000, '100000' => 100000); $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'tool_uploadcourse'), $choices); $mform->setType('previewrows', PARAM_INT); $mform->addHelpButton('previewrows', 'rowpreviewnum', 'tool_uploadcourse'); $this->add_import_options(); $mform->addElement('hidden', 'showpreview', 1); $mform->setType('showpreview', PARAM_INT); $this->add_action_buttons(false, get_string('preview', 'tool_uploadcourse')); } } output/questionname.php 0000644 00000003451 15152154613 0011333 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace qbank_viewquestionname\output; use core\output\inplace_editable; use core\output\named_templatable; use renderable; /** * Question in place editing api call. * * @package qbank_viewquestionname * @copyright 2022 Catalyst IT Australia Pty Ltd * @author Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class questionname extends inplace_editable implements named_templatable, renderable { public function __construct(\stdClass $question) { parent::__construct( 'qbank_viewquestionname', 'questionname', $question->id, question_has_capability_on($question, 'edit'), format_string($question->name), $question->name, get_string('edit_question_name_hint', 'qbank_viewquestionname'), get_string('edit_question_name_label', 'qbank_viewquestionname', (object) [ 'name' => $question->name, ]) ); } public function get_template_name(\renderer_base $renderer): string { return 'core/inplace_editable'; } } question_name_idnumber_tags_column.php 0000644 00000006533 15152154613 0014416 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace qbank_viewquestionname; /** * A question bank column showing the question name with idnumber and tags. * * @package qbank_viewquestionname * @copyright 2019 The Open University * @author 2021 Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class question_name_idnumber_tags_column extends viewquestionname_column_helper { public function get_name(): string { return 'qnameidnumbertags'; } protected function display_content($question, $rowclasses): void { global $OUTPUT; echo \html_writer::start_tag('div', ['class' => 'd-inline-flex flex-nowrap overflow-hidden w-100']); $questiondisplay = $OUTPUT->render(new \qbank_viewquestionname\output\questionname($question)); $labelfor = $this->label_for($question); if ($labelfor) { echo \html_writer::tag('label', $questiondisplay, [ 'for' => $labelfor, ]); } else { echo \html_writer::start_span('questionname flex-grow-1 flex-shrink-1 text-truncate'); echo $questiondisplay; echo \html_writer::end_span(); } // Question idnumber. // The non-breaking space ' ' is used in html to fix MDL-75051 (browser issues caused by chrome and Edge). if ($question->idnumber !== null && $question->idnumber !== '') { echo ' ' . \html_writer::span( \html_writer::span(get_string('idnumber', 'question'), 'accesshide') . ' ' . \html_writer::span(s($question->idnumber), 'badge badge-primary'), 'ml-1'); } // Question tags. if (!empty($question->tags)) { $tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id); echo $OUTPUT->tag_list($tags, null, 'd-inline flex-shrink-1 text-truncate ml-1', 0, null, true); } echo \html_writer::end_tag('div'); } public function get_required_fields(): array { $fields = parent::get_required_fields(); $fields[] = 'qbe.idnumber'; return $fields; } public function is_sortable(): array { return [ 'name' => ['field' => 'q.name', 'title' => get_string('questionname', 'question')], 'idnumber' => ['field' => 'qbe.idnumber', 'title' => get_string('idnumber', 'question')], ]; } public function load_additional_data(array $questions): void { parent::load_additional_data($questions); parent::load_question_tags($questions); } public function get_extra_classes(): array { return ['pr-3']; } } viewquestionname_column_helper.php 0000644 00000004232 15152154613 0013600 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace qbank_viewquestionname; use core_question\local\bank\column_base; /** * A column type for the name of the question name. * * @package qbank_viewquestionname * @copyright 2009 Tim Hunt * @author 2021 Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class viewquestionname_column_helper extends column_base { /** * @var null $checkboxespresent */ protected $checkboxespresent = null; public function get_name(): string { return 'questionname'; } public function get_title(): string { return get_string('question'); } protected function label_for($question): string { if (is_null($this->checkboxespresent)) { $this->checkboxespresent = $this->qbank->has_column('checkbox_column'); } if ($this->checkboxespresent) { return 'checkq' . $question->id; } return ''; } protected function display_content($question, $rowclasses): void { $labelfor = $this->label_for($question); if ($labelfor) { echo \html_writer::start_tag('label', array('for' => $labelfor)); } echo format_string($question->name); if ($labelfor) { echo \html_writer::end_tag('label'); } } public function get_required_fields(): array { return ['q.id', 'q.name']; } public function is_sortable() { return 'q.name'; } } plugin_feature.php 0000644 00000002336 15152154613 0010275 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace qbank_viewquestionname; use core_question\local\bank\plugin_features_base; /** * Plugin entrypoint for columns. * * @package qbank_viewquestionname * @copyright 2021 Catalyst IT Australia Pty Ltd * @author Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plugin_feature extends plugin_features_base { public function get_question_columns($qbank): array { return [ new question_name_idnumber_tags_column($qbank) ]; } } schema.php 0000644 00000027450 15152167354 0006536 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Solr schema manipulation manager. * * @package search_solr * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace search_solr; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/lib/filelib.php'); /** * Schema class to interact with Solr schema. * * At the moment it only implements create which should be enough for a basic * moodle configuration in Solr. * * @package search_solr * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class schema { /** * @var stdClass */ protected $config = null; /** * cUrl instance. * @var \curl */ protected $curl = null; /** * An engine instance. * @var engine */ protected $engine = null; /** * Constructor. * * @param engine $engine Optional engine parameter, if not specified then one will be created * @throws \moodle_exception * @return void */ public function __construct(engine $engine = null) { if (!$this->config = get_config('search_solr')) { throw new \moodle_exception('missingconfig', 'search_solr'); } if (empty($this->config->server_hostname) || empty($this->config->indexname)) { throw new \moodle_exception('missingconfig', 'search_solr'); } $this->engine = $engine ?? new engine(); $this->curl = $this->engine->get_curl_object(); // HTTP headers. $this->curl->setHeader('Content-type: application/json'); } /** * Can setup be executed against the configured server. * * @return true|string True or error message. */ public function can_setup_server() { $status = $this->engine->is_server_configured(); if ($status !== true) { return $status; } // At this stage we know that the server is properly configured with a valid host:port and indexname. // We're not too concerned about repeating the SolrClient::system() call (already called in // is_server_configured) because this is just a setup script. if ($this->engine->get_solr_major_version() < 5) { // Schema setup script only available for 5.0 onwards. return get_string('schemasetupfromsolr5', 'search_solr'); } return true; } /** * Setup solr stuff required by moodle. * * @param bool $checkexisting Whether to check if the fields already exist or not * @return bool */ public function setup($checkexisting = true) { $fields = \search_solr\document::get_default_fields_definition(); // Field id is already there. unset($fields['id']); $this->check_index(); $return = $this->add_fields($fields, $checkexisting); // Tell the engine we are now using the latest schema version. $this->engine->record_applied_schema_version(document::SCHEMA_VERSION); return $return; } /** * Checks the schema is properly set up. * * @throws \moodle_exception * @return void */ public function validate_setup() { $fields = \search_solr\document::get_default_fields_definition(); // Field id is already there. unset($fields['id']); $this->check_index(); $this->validate_fields($fields, true); } /** * Checks if the index is ready, triggers an exception otherwise. * * @throws \moodle_exception * @return void */ protected function check_index() { // Check that the server is available and the index exists. $url = $this->engine->get_connection_url('/select?wt=json'); $result = $this->curl->get($url); if ($this->curl->error) { throw new \moodle_exception('connectionerror', 'search_solr'); } if ($this->curl->info['http_code'] === 404) { throw new \moodle_exception('connectionerror', 'search_solr'); } } /** * Adds the provided fields to Solr schema. * * Intentionally separated from create(), it can be called to add extra fields. * fields separately. * * @throws \coding_exception * @throws \moodle_exception * @param array $fields \core_search\document::$requiredfields format * @param bool $checkexisting Whether to check if the fields already exist or not * @return bool */ protected function add_fields($fields, $checkexisting = true) { if ($checkexisting) { // Check that non of them exists. $this->validate_fields($fields, false); } $url = $this->engine->get_connection_url('/schema'); // Add all fields. foreach ($fields as $fieldname => $data) { if (!isset($data['type']) || !isset($data['stored']) || !isset($data['indexed'])) { throw new \coding_exception($fieldname . ' does not define all required field params: type, stored and indexed.'); } $type = $this->doc_field_to_solr_field($data['type']); // Changing default multiValued value to false as we want to match values easily. $params = array( 'add-field' => array( 'name' => $fieldname, 'type' => $type, 'stored' => $data['stored'], 'multiValued' => false, 'indexed' => $data['indexed'] ) ); $results = $this->curl->post($url, json_encode($params)); // We only validate if we are interested on it. if ($checkexisting) { if ($this->curl->error) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error); } $this->validate_add_field_result($results); } } return true; } /** * Checks if the schema existing fields are properly set, triggers an exception otherwise. * * @throws \moodle_exception * @param array $fields * @param bool $requireexisting Require the fields to exist, otherwise exception. * @return void */ protected function validate_fields(&$fields, $requireexisting = false) { global $CFG; foreach ($fields as $fieldname => $data) { $url = $this->engine->get_connection_url('/schema/fields/' . $fieldname); $results = $this->curl->get($url); if ($this->curl->error) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error); } if (!$results) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr')); } $results = json_decode($results); if ($requireexisting && !empty($results->error) && $results->error->code === 404) { $a = new \stdClass(); $a->fieldname = $fieldname; $a->setupurl = $CFG->wwwroot . '/search/engine/solr/setup_schema.php'; throw new \moodle_exception('errorvalidatingschema', 'search_solr', '', $a); } // The field should not exist so we only accept 404 errors. if (empty($results->error) || (!empty($results->error) && $results->error->code !== 404)) { if (!empty($results->error)) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error->msg); } else { // All these field attributes are set when fields are added through this script and should // be returned and match the defined field's values. $expectedsolrfield = $this->doc_field_to_solr_field($data['type']); if (empty($results->field) || !isset($results->field->type) || !isset($results->field->multiValued) || !isset($results->field->indexed) || !isset($results->field->stored)) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('schemafieldautocreated', 'search_solr', $fieldname)); } else if ($results->field->type !== $expectedsolrfield || $results->field->multiValued !== false || $results->field->indexed !== $data['indexed'] || $results->field->stored !== $data['stored']) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('schemafieldautocreated', 'search_solr', $fieldname)); } else { // The field already exists and it is properly defined, no need to create it. unset($fields[$fieldname]); } } } } } /** * Checks that the field results do not contain errors. * * @throws \moodle_exception * @param string $results curl response body * @return void */ protected function validate_add_field_result($result) { if (!$result) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr')); } $results = json_decode($result); if (!$results) { if (is_scalar($result)) { $errormsg = $result; } else { $errormsg = json_encode($result); } throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errormsg); } // It comes as error when fetching fields data. if (!empty($results->error)) { throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error); } // It comes as errors when adding fields. if (!empty($results->errors)) { // We treat this error separately. $errorstr = ''; foreach ($results->errors as $error) { $errorstr .= implode(', ', $error->errorMessages); } throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errorstr); } } /** * Returns the solr field type from the document field type string. * * @param string $datatype * @return string */ private function doc_field_to_solr_field($datatype) { $type = $datatype; $solrversion = $this->engine->get_solr_major_version(); switch($datatype) { case 'text': $type = 'text_general'; break; case 'int': if ($solrversion >= 7) { $type = 'pint'; } break; case 'tdate': if ($solrversion >= 7) { $type = 'pdate'; } break; } return $type; } } engine.php 0000644 00000170263 15152167354 0006544 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Solr engine. * * @package search_solr * @copyright 2015 Daniel Neis Araujo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace search_solr; defined('MOODLE_INTERNAL') || die(); /** * Solr engine. * * @package search_solr * @copyright 2015 Daniel Neis Araujo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class engine extends \core_search\engine { /** * @var string The date format used by solr. */ const DATE_FORMAT = 'Y-m-d\TH:i:s\Z'; /** * @var int Commit documents interval (number of miliseconds). */ const AUTOCOMMIT_WITHIN = 15000; /** * The maximum number of results to fetch at a time. */ const QUERY_SIZE = 120; /** * Highlighting fragsize. Slightly larger than output size (500) to allow for ... appending. */ const FRAG_SIZE = 510; /** * Marker for the start of a highlight. */ const HIGHLIGHT_START = '@@HI_S@@'; /** * Marker for the end of a highlight. */ const HIGHLIGHT_END = '@@HI_E@@'; /** @var float Boost value for matching course in location-ordered searches */ const COURSE_BOOST = 1; /** @var float Boost value for matching context (in addition to course boost) */ const CONTEXT_BOOST = 0.5; /** * @var \SolrClient */ protected $client = null; /** * @var bool True if we should reuse SolrClients, false if not. */ protected $cacheclient = true; /** * @var \curl Direct curl object. */ protected $curl = null; /** * @var array Fields that can be highlighted. */ protected $highlightfields = array('title', 'content', 'description1', 'description2'); /** * @var int Number of total docs reported by Sorl for the last query. */ protected $totalenginedocs = 0; /** * @var int Number of docs we have processed for the last query. */ protected $processeddocs = 0; /** * @var int Number of docs that have been skipped while processing the last query. */ protected $skippeddocs = 0; /** * Solr server major version. * * @var int */ protected $solrmajorversion = null; /** * Initialises the search engine configuration. * * @param bool $alternateconfiguration If true, use alternate configuration settings * @return void */ public function __construct(bool $alternateconfiguration = false) { parent::__construct($alternateconfiguration); $curlversion = curl_version(); if (isset($curlversion['version']) && stripos($curlversion['version'], '7.35.') === 0) { // There is a flaw with curl 7.35.0 that causes problems with client reuse. $this->cacheclient = false; } } /** * Prepares a Solr query, applies filters and executes it returning its results. * * @throws \core_search\engine_exception * @param \stdClass $filters Containing query and filters. * @param \stdClass $accessinfo Information about areas user can access. * @param int $limit The maximum number of results to return. * @return \core_search\document[] Results or false if no results */ public function execute_query($filters, $accessinfo, $limit = 0) { global $USER; if (empty($limit)) { $limit = \core_search\manager::MAX_RESULTS; } // If there is any problem we trigger the exception as soon as possible. $client = $this->get_search_client(); // Create the query object. $query = $this->create_user_query($filters, $accessinfo); // If the query cannot have results, return none. if (!$query) { return []; } // We expect good match rates, so for our first get, we will get a small number of records. // This significantly speeds solr response time for first few pages. $query->setRows(min($limit * 3, static::QUERY_SIZE)); $response = $this->get_query_response($query); // Get count data out of the response, and reset our counters. list($included, $found) = $this->get_response_counts($response); $this->totalenginedocs = $found; $this->processeddocs = 0; $this->skippeddocs = 0; if ($included == 0 || $this->totalenginedocs == 0) { // No results. return array(); } // Get valid documents out of the response. $results = $this->process_response($response, $limit); // We have processed all the docs in the response at this point. $this->processeddocs += $included; // If we haven't reached the limit, and there are more docs left in Solr, lets keep trying. while (count($results) < $limit && ($this->totalenginedocs - $this->processeddocs) > 0) { // Offset the start of the query, and since we are making another call, get more per call. $query->setStart($this->processeddocs); $query->setRows(static::QUERY_SIZE); $response = $this->get_query_response($query); list($included, $found) = $this->get_response_counts($response); if ($included == 0 || $found == 0) { // No new results were found. Found being empty would be weird, so we will just return. return $results; } $this->totalenginedocs = $found; // Get the new response docs, limiting to remaining we need, then add it to the end of the results array. $newdocs = $this->process_response($response, $limit - count($results)); $results = array_merge($results, $newdocs); // Add to our processed docs count. $this->processeddocs += $included; } return $results; } /** * Takes a query and returns the response in SolrObject format. * * @param SolrQuery $query Solr query object. * @return SolrObject|false Response document or false on error. */ protected function get_query_response($query) { try { return $this->get_search_client()->query($query)->getResponse(); } catch (\SolrClientException $ex) { debugging('Error executing the provided query: ' . $ex->getMessage(), DEBUG_DEVELOPER); $this->queryerror = $ex->getMessage(); return false; } catch (\SolrServerException $ex) { debugging('Error executing the provided query: ' . $ex->getMessage(), DEBUG_DEVELOPER); $this->queryerror = $ex->getMessage(); return false; } } /** * Returns the total number of documents available for the most recently call to execute_query. * * @return int */ public function get_query_total_count() { // Return the total engine count minus the docs we have determined are bad. return $this->totalenginedocs - $this->skippeddocs; } /** * Returns count information for a provided response. Will return 0, 0 for invalid or empty responses. * * @param SolrDocument $response The response document from Solr. * @return array A two part array. First how many response docs are in the response. * Second, how many results are vailable in the engine. */ protected function get_response_counts($response) { $found = 0; $included = 0; if (isset($response->grouped->solr_filegroupingid->ngroups)) { // Get the number of results for file grouped queries. $found = $response->grouped->solr_filegroupingid->ngroups; $included = count($response->grouped->solr_filegroupingid->groups); } else if (isset($response->response->numFound)) { // Get the number of results for standard queries. $found = $response->response->numFound; if ($found > 0 && is_array($response->response->docs)) { $included = count($response->response->docs); } } return array($included, $found); } /** * Prepares a new query object with needed limits, filters, etc. * * @param \stdClass $filters Containing query and filters. * @param \stdClass $accessinfo Information about contexts the user can access * @return \SolrDisMaxQuery|null Query object or null if they can't get any results */ protected function create_user_query($filters, $accessinfo) { global $USER; // Let's keep these changes internal. $data = clone $filters; $query = new \SolrDisMaxQuery(); $this->set_query($query, self::replace_underlines($data->q)); $this->add_fields($query); // Search filters applied, we don't cache these filters as we don't want to pollute the cache with tmp filters // we are really interested in caching contexts filters instead. if (!empty($data->title)) { $query->addFilterQuery('{!field cache=false f=title}' . $data->title); } if (!empty($data->areaids)) { // If areaids are specified, we want to get any that match. $query->addFilterQuery('{!cache=false}areaid:(' . implode(' OR ', $data->areaids) . ')'); } if (!empty($data->courseids)) { $query->addFilterQuery('{!cache=false}courseid:(' . implode(' OR ', $data->courseids) . ')'); } if (!empty($data->groupids)) { $query->addFilterQuery('{!cache=false}groupid:(' . implode(' OR ', $data->groupids) . ')'); } if (!empty($data->userids)) { $query->addFilterQuery('{!cache=false}userid:(' . implode(' OR ', $data->userids) . ')'); } if (!empty($data->timestart) or !empty($data->timeend)) { if (empty($data->timestart)) { $data->timestart = '*'; } else { $data->timestart = \search_solr\document::format_time_for_engine($data->timestart); } if (empty($data->timeend)) { $data->timeend = '*'; } else { $data->timeend = \search_solr\document::format_time_for_engine($data->timeend); } // No cache. $query->addFilterQuery('{!cache=false}modified:[' . $data->timestart . ' TO ' . $data->timeend . ']'); } // Restrict to users who are supposed to be able to see a particular result. $query->addFilterQuery('owneruserid:(' . \core_search\manager::NO_OWNER_ID . ' OR ' . $USER->id . ')'); // And finally restrict it to the context where the user can access, we want this one cached. // If the user can access all contexts $usercontexts value is just true, we don't need to filter // in that case. if (!$accessinfo->everything && is_array($accessinfo->usercontexts)) { // Join all area contexts into a single array and implode. $allcontexts = array(); foreach ($accessinfo->usercontexts as $areaid => $areacontexts) { if (!empty($data->areaids) && !in_array($areaid, $data->areaids)) { // Skip unused areas. continue; } foreach ($areacontexts as $contextid) { // Ensure they are unique. $allcontexts[$contextid] = $contextid; } } if (empty($allcontexts)) { // This means there are no valid contexts for them, so they get no results. return null; } $query->addFilterQuery('contextid:(' . implode(' OR ', $allcontexts) . ')'); } if (!$accessinfo->everything && $accessinfo->separategroupscontexts) { // Add another restriction to handle group ids. If there are any contexts using separate // groups, then results in that context will not show unless you belong to the group. // (Note: Access all groups is taken care of earlier, when computing these arrays.) // This special exceptions list allows for particularly pig-headed developers to create // multiple search areas within the same module, where one of them uses separate // groups and the other uses visible groups. It is a little inefficient, but this should // be rare. $exceptions = ''; if ($accessinfo->visiblegroupscontextsareas) { foreach ($accessinfo->visiblegroupscontextsareas as $contextid => $areaids) { $exceptions .= ' OR (contextid:' . $contextid . ' AND areaid:(' . implode(' OR ', $areaids) . '))'; } } if ($accessinfo->usergroups) { // Either the document has no groupid, or the groupid is one that the user // belongs to, or the context is not one of the separate groups contexts. $query->addFilterQuery('(*:* -groupid:[* TO *]) OR ' . 'groupid:(' . implode(' OR ', $accessinfo->usergroups) . ') OR ' . '(*:* -contextid:(' . implode(' OR ', $accessinfo->separategroupscontexts) . '))' . $exceptions); } else { // Either the document has no groupid, or the context is not a restricted one. $query->addFilterQuery('(*:* -groupid:[* TO *]) OR ' . '(*:* -contextid:(' . implode(' OR ', $accessinfo->separategroupscontexts) . '))' . $exceptions); } } if ($this->file_indexing_enabled()) { // Now group records by solr_filegroupingid. Limit to 3 results per group. $query->setGroup(true); $query->setGroupLimit(3); $query->setGroupNGroups(true); $query->addGroupField('solr_filegroupingid'); } else { // Make sure we only get text files, in case the index has pre-existing files. $query->addFilterQuery('type:'.\core_search\manager::TYPE_TEXT); } // If ordering by location, add in boost for the relevant course or context ids. if (!empty($filters->order) && $filters->order === 'location') { $coursecontext = $filters->context->get_course_context(); $query->addBoostQuery('courseid', $coursecontext->instanceid, self::COURSE_BOOST); if ($filters->context->contextlevel !== CONTEXT_COURSE) { // If it's a block or activity, also add a boost for the specific context id. $query->addBoostQuery('contextid', $filters->context->id, self::CONTEXT_BOOST); } } return $query; } /** * Prepares a new query by setting the query, start offset and rows to return. * * @param SolrQuery $query * @param object $q Containing query and filters. */ protected function set_query($query, $q) { // Set hightlighting. $query->setHighlight(true); foreach ($this->highlightfields as $field) { $query->addHighlightField($field); } $query->setHighlightFragsize(static::FRAG_SIZE); $query->setHighlightSimplePre(self::HIGHLIGHT_START); $query->setHighlightSimplePost(self::HIGHLIGHT_END); $query->setHighlightMergeContiguous(true); $query->setQuery($q); // A reasonable max. $query->setRows(static::QUERY_SIZE); } /** * Sets fields to be returned in the result. * * @param SolrDisMaxQuery|SolrQuery $query object. */ public function add_fields($query) { $documentclass = $this->get_document_classname(); $fields = $documentclass::get_default_fields_definition(); $dismax = false; if ($query instanceof \SolrDisMaxQuery) { $dismax = true; } foreach ($fields as $key => $field) { $query->addField($key); if ($dismax && !empty($field['mainquery'])) { // Add fields the main query should be run against. // Due to a regression in the PECL solr extension, https://bugs.php.net/bug.php?id=72740, // a boost value is required, even if it is optional; to avoid boosting one among other fields, // the explicit boost value will be the default one, for every field. $query->addQueryField($key, 1); } } } /** * Finds the key common to both highlighing and docs array returned from response. * @param object $response containing results. */ public function add_highlight_content($response) { if (!isset($response->highlighting)) { // There is no highlighting to add. return; } $highlightedobject = $response->highlighting; foreach ($response->response->docs as $doc) { $x = $doc->id; $highlighteddoc = $highlightedobject->$x; $this->merge_highlight_field_values($doc, $highlighteddoc); } } /** * Adds the highlighting array values to docs array values. * * @throws \core_search\engine_exception * @param object $doc containing the results. * @param object $highlighteddoc containing the highlighted results values. */ public function merge_highlight_field_values($doc, $highlighteddoc) { foreach ($this->highlightfields as $field) { if (!empty($doc->$field)) { // Check that the returned value is not an array. No way we can make this work with multivalued solr fields. if (is_array($doc->{$field})) { throw new \core_search\engine_exception('multivaluedfield', 'search_solr', '', $field); } if (!empty($highlighteddoc->$field)) { // Replace by the highlighted result. $doc->$field = reset($highlighteddoc->$field); } } } } /** * Filters the response on Moodle side. * * @param SolrObject $response Solr object containing the response return from solr server. * @param int $limit The maximum number of results to return. 0 for all. * @param bool $skipaccesscheck Don't use check_access() on results. Only to be used when results have known access. * @return array $results containing final results to be displayed. */ protected function process_response($response, $limit = 0, $skipaccesscheck = false) { global $USER; if (empty($response)) { return array(); } if (isset($response->grouped)) { return $this->grouped_files_process_response($response, $limit); } $userid = $USER->id; $noownerid = \core_search\manager::NO_OWNER_ID; $numgranted = 0; if (!$docs = $response->response->docs) { return array(); } $out = array(); if (!empty($response->response->numFound)) { $this->add_highlight_content($response); // Iterate through the results checking its availability and whether they are available for the user or not. foreach ($docs as $key => $docdata) { if ($docdata['owneruserid'] != $noownerid && $docdata['owneruserid'] != $userid) { // If owneruserid is set, no other user should be able to access this record. continue; } if (!$searcharea = $this->get_search_area($docdata->areaid)) { continue; } $docdata = $this->standarize_solr_obj($docdata); if ($skipaccesscheck) { $access = \core_search\manager::ACCESS_GRANTED; } else { $access = $searcharea->check_access($docdata['itemid']); } switch ($access) { case \core_search\manager::ACCESS_DELETED: $this->delete_by_id($docdata['id']); // Remove one from our processed and total counters, since we promptly deleted. $this->processeddocs--; $this->totalenginedocs--; break; case \core_search\manager::ACCESS_DENIED: $this->skippeddocs++; break; case \core_search\manager::ACCESS_GRANTED: $numgranted++; // Add the doc. $out[] = $this->to_document($searcharea, $docdata); break; } // Stop when we hit our limit. if (!empty($limit) && count($out) >= $limit) { break; } } } return $out; } /** * Processes grouped file results into documents, with attached matching files. * * @param SolrObject $response The response returned from solr server * @param int $limit The maximum number of results to return. 0 for all. * @return array Final results to be displayed. */ protected function grouped_files_process_response($response, $limit = 0) { // If we can't find the grouping, or there are no matches in the grouping, return empty. if (!isset($response->grouped->solr_filegroupingid) || empty($response->grouped->solr_filegroupingid->matches)) { return array(); } $numgranted = 0; $orderedids = array(); $completedocs = array(); $incompletedocs = array(); $highlightingobj = $response->highlighting; // Each group represents a "master document". $groups = $response->grouped->solr_filegroupingid->groups; foreach ($groups as $group) { $groupid = $group->groupValue; $groupdocs = $group->doclist->docs; $firstdoc = reset($groupdocs); if (!$searcharea = $this->get_search_area($firstdoc->areaid)) { // Well, this is a problem. continue; } // Check for access. $access = $searcharea->check_access($firstdoc->itemid); switch ($access) { case \core_search\manager::ACCESS_DELETED: // If deleted from Moodle, delete from index and then continue. $this->delete_by_id($firstdoc->id); // Remove one from our processed and total counters, since we promptly deleted. $this->processeddocs--; $this->totalenginedocs--; continue 2; break; case \core_search\manager::ACCESS_DENIED: // This means we should just skip for the current user. $this->skippeddocs++; continue 2; break; } $numgranted++; $maindoc = false; $fileids = array(); // Seperate the main document and any files returned. foreach ($groupdocs as $groupdoc) { if ($groupdoc->id == $groupid) { $maindoc = $groupdoc; } else if (isset($groupdoc->solr_fileid)) { $fileids[] = $groupdoc->solr_fileid; } } // Store the id of this group, in order, for later merging. $orderedids[] = $groupid; if (!$maindoc) { // We don't have the main doc, store what we know for later building. $incompletedocs[$groupid] = $fileids; } else { if (isset($highlightingobj->$groupid)) { // Merge the highlighting for this doc. $this->merge_highlight_field_values($maindoc, $highlightingobj->$groupid); } $docdata = $this->standarize_solr_obj($maindoc); $doc = $this->to_document($searcharea, $docdata); // Now we need to attach the result files to the doc. foreach ($fileids as $fileid) { $doc->add_stored_file($fileid); } $completedocs[$groupid] = $doc; } if (!empty($limit) && $numgranted >= $limit) { // We have hit the max results, we will just ignore the rest. break; } } $incompletedocs = $this->get_missing_docs($incompletedocs); $out = array(); // Now merge the complete and incomplete documents, in results order. foreach ($orderedids as $docid) { if (isset($completedocs[$docid])) { $out[] = $completedocs[$docid]; } else if (isset($incompletedocs[$docid])) { $out[] = $incompletedocs[$docid]; } } return $out; } /** * Retreive any missing main documents and attach provided files. * * The missingdocs array should be an array, indexed by document id, of main documents we need to retrieve. The value * associated to the key should be an array of stored_files or stored file ids to attach to the result document. * * Return array also indexed by document id. * * @param array() $missingdocs An array, indexed by document id, with arrays of files/ids to attach. * @return document[] */ protected function get_missing_docs($missingdocs) { if (empty($missingdocs)) { return array(); } $docids = array_keys($missingdocs); // Build a custom query that will get all the missing documents. $query = new \SolrQuery(); $this->set_query($query, '*'); $this->add_fields($query); $query->setRows(count($docids)); $query->addFilterQuery('{!cache=false}id:(' . implode(' OR ', $docids) . ')'); $response = $this->get_query_response($query); // We know the missing docs have already been checked for access, so don't recheck. $results = $this->process_response($response, 0, true); $out = array(); foreach ($results as $result) { $resultid = $result->get('id'); if (!isset($missingdocs[$resultid])) { // We got a result we didn't expect. Skip it. continue; } // Attach the files. foreach ($missingdocs[$resultid] as $filedoc) { $result->add_stored_file($filedoc); } $out[$resultid] = $result; } return $out; } /** * Returns a standard php array from a \SolrObject instance. * * @param \SolrObject $obj * @return array The returned document as an array. */ public function standarize_solr_obj(\SolrObject $obj) { $properties = $obj->getPropertyNames(); $docdata = array(); foreach($properties as $name) { // http://php.net/manual/en/solrobject.getpropertynames.php#98018. $name = trim($name); $docdata[$name] = $obj->offsetGet($name); } return $docdata; } /** * Adds a document to the search engine. * * This does not commit to the search engine. * * @param document $document * @param bool $fileindexing True if file indexing is to be used * @return bool */ public function add_document($document, $fileindexing = false) { $docdata = $document->export_for_engine(); if (!$this->add_solr_document($docdata)) { return false; } if ($fileindexing) { // This will take care of updating all attached files in the index. $this->process_document_files($document); } return true; } /** * Adds a batch of documents to the engine at once. * * @param \core_search\document[] $documents Documents to add * @param bool $fileindexing If true, indexes files (these are done one at a time) * @return int[] Array of three elements: successfully processed, failed processed, batch count */ public function add_document_batch(array $documents, bool $fileindexing = false): array { $docdatabatch = []; foreach ($documents as $document) { $docdatabatch[] = $document->export_for_engine(); } $resultcounts = $this->add_solr_documents($docdatabatch); // Files are processed one document at a time (if there are files it's slow anyway). if ($fileindexing) { foreach ($documents as $document) { // This will take care of updating all attached files in the index. $this->process_document_files($document); } } return $resultcounts; } /** * Replaces underlines at edges of words in the content with spaces. * * For example '_frogs_' will become 'frogs', '_frogs and toads_' will become 'frogs and toads', * and 'frogs_and_toads' will be left as 'frogs_and_toads'. * * The reason for this is that for italic content_to_text puts _italic_ underlines at the start * and end of the italicised phrase (not between words). Solr treats underlines as part of the * word, which means that if you search for a word in italic then you can't find it. * * @param string $str String to replace * @return string Replaced string */ protected static function replace_underlines(string $str): string { return preg_replace('~\b_|_\b~', '', $str); } /** * Creates a Solr document object. * * @param array $doc Array of document fields * @return \SolrInputDocument Created document */ protected function create_solr_document(array $doc): \SolrInputDocument { $solrdoc = new \SolrInputDocument(); // Replace underlines in the content with spaces. The reason for this is that for italic // text, content_to_text puts _italic_ underlines. Solr treats underlines as part of the // word, which means that if you search for a word in italic then you can't find it. if (array_key_exists('content', $doc)) { $doc['content'] = self::replace_underlines($doc['content']); } // Set all the fields. foreach ($doc as $field => $value) { $solrdoc->addField($field, $value); } return $solrdoc; } /** * Adds a text document to the search engine. * * @param array $doc * @return bool */ protected function add_solr_document($doc) { $solrdoc = $this->create_solr_document($doc); try { $result = $this->get_search_client()->addDocument($solrdoc, true, static::AUTOCOMMIT_WITHIN); return true; } catch (\SolrClientException $e) { debugging('Solr client error adding document with id ' . $doc['id'] . ': ' . $e->getMessage(), DEBUG_DEVELOPER); } catch (\SolrServerException $e) { // We only use the first line of the message, as it's a fully java stacktrace behind it. $msg = strtok($e->getMessage(), "\n"); debugging('Solr server error adding document with id ' . $doc['id'] . ': ' . $msg, DEBUG_DEVELOPER); } return false; } /** * Adds multiple text documents to the search engine. * * @param array $docs Array of documents (each an array of fields) to add * @return int[] Array of success, failure, batch count * @throws \core_search\engine_exception */ protected function add_solr_documents(array $docs): array { $solrdocs = []; foreach ($docs as $doc) { $solrdocs[] = $this->create_solr_document($doc); } try { // Add documents in a batch and report that they all succeeded. $this->get_search_client()->addDocuments($solrdocs, true, static::AUTOCOMMIT_WITHIN); return [count($solrdocs), 0, 1]; } catch (\SolrClientException $e) { // If there is an exception, fall through... $donothing = true; } catch (\SolrServerException $e) { // If there is an exception, fall through... $donothing = true; } // When there is an error, we fall back to adding them individually so that we can report // which document(s) failed. Since it overwrites, adding the successful ones multiple // times won't hurt. $success = 0; $failure = 0; $batches = 0; foreach ($docs as $doc) { $result = $this->add_solr_document($doc); $batches++; if ($result) { $success++; } else { $failure++; } } return [$success, $failure, $batches]; } /** * Index files attached to the docuemnt, ensuring the index matches the current document files. * * For documents that aren't known to be new, we check the index for existing files. * - New files we will add. * - Existing and unchanged files we will skip. * - File that are in the index but not on the document will be deleted from the index. * - Files that have changed will be re-indexed. * * @param document $document */ protected function process_document_files($document) { if (!$this->file_indexing_enabled()) { return; } // Maximum rows to process at a time. $rows = 500; // Get the attached files. $files = $document->get_files(); // If this isn't a new document, we need to check the exiting indexed files. if (!$document->get_is_new()) { // We do this progressively, so we can handle lots of files cleanly. list($numfound, $indexedfiles) = $this->get_indexed_files($document, 0, $rows); $count = 0; $idstodelete = array(); do { // Go through each indexed file. We want to not index any stored and unchanged ones, delete any missing ones. foreach ($indexedfiles as $indexedfile) { $fileid = $indexedfile->solr_fileid; if (isset($files[$fileid])) { // Check for changes that would mean we need to re-index the file. If so, just leave in $files. // Filelib does not guarantee time modified is updated, so we will check important values. if ($indexedfile->modified != $files[$fileid]->get_timemodified()) { continue; } if (strcmp($indexedfile->title, $files[$fileid]->get_filename()) !== 0) { continue; } if ($indexedfile->solr_filecontenthash != $files[$fileid]->get_contenthash()) { continue; } if ($indexedfile->solr_fileindexstatus == document::INDEXED_FILE_FALSE && $this->file_is_indexable($files[$fileid])) { // This means that the last time we indexed this file, filtering blocked it. // Current settings say it is indexable, so we will allow it to be indexed. continue; } // If the file is already indexed, we can just remove it from the files array and skip it. unset($files[$fileid]); } else { // This means we have found a file that is no longer attached, so we need to delete from the index. // We do it later, since this is progressive, and it could reorder results. $idstodelete[] = $indexedfile->id; } } $count += $rows; if ($count < $numfound) { // If we haven't hit the total count yet, fetch the next batch. list($numfound, $indexedfiles) = $this->get_indexed_files($document, $count, $rows); } } while ($count < $numfound); // Delete files that are no longer attached. foreach ($idstodelete as $id) { // We directly delete the item using the client, as the engine delete_by_id won't work on file docs. $this->get_search_client()->deleteById($id); } } // Now we can actually index all the remaining files. foreach ($files as $file) { $this->add_stored_file($document, $file); } } /** * Get the currently indexed files for a particular document, returns the total count, and a subset of files. * * @param document $document * @param int $start The row to start the results on. Zero indexed. * @param int $rows The number of rows to fetch * @return array A two element array, the first is the total number of availble results, the second is an array * of documents for the current request. */ protected function get_indexed_files($document, $start = 0, $rows = 500) { // Build a custom query that will get any document files that are in our solr_filegroupingid. $query = new \SolrQuery(); // We want to get all file records tied to a document. // For efficiency, we are building our own, stripped down, query. $query->setQuery('*'); $query->setRows($rows); $query->setStart($start); // We want a consistent sorting. $query->addSortField('id'); // We only want the bare minimum of fields. $query->addField('id'); $query->addField('modified'); $query->addField('title'); $query->addField('solr_fileid'); $query->addField('solr_filecontenthash'); $query->addField('solr_fileindexstatus'); $query->addFilterQuery('{!cache=false}solr_filegroupingid:(' . $document->get('id') . ')'); $query->addFilterQuery('type:' . \core_search\manager::TYPE_FILE); $response = $this->get_query_response($query); if (empty($response->response->numFound)) { return array(0, array()); } return array($response->response->numFound, $this->convert_file_results($response)); } /** * A very lightweight handler for getting information about already indexed files from a Solr response. * * @param SolrObject $responsedoc A Solr response document * @return stdClass[] An array of objects that contain the basic information for file processing. */ protected function convert_file_results($responsedoc) { if (!$docs = $responsedoc->response->docs) { return array(); } $out = array(); foreach ($docs as $doc) { // Copy the bare minimim needed info. $result = new \stdClass(); $result->id = $doc->id; $result->modified = document::import_time_from_engine($doc->modified); $result->title = $doc->title; $result->solr_fileid = $doc->solr_fileid; $result->solr_filecontenthash = $doc->solr_filecontenthash; $result->solr_fileindexstatus = $doc->solr_fileindexstatus; $out[] = $result; } return $out; } /** * Adds a file to the search engine. * * Notes about Solr and Tika indexing. We do not send the mime type, only the filename. * Tika has much better content type detection than Moodle, and we will have many more doc failures * if we try to send mime types. * * @param document $document * @param \stored_file $storedfile * @return void */ protected function add_stored_file($document, $storedfile) { $filedoc = $document->export_file_for_engine($storedfile); if (!$this->file_is_indexable($storedfile)) { // For files that we don't consider indexable, we will still place a reference in the search engine. $filedoc['solr_fileindexstatus'] = document::INDEXED_FILE_FALSE; $this->add_solr_document($filedoc); return; } $curl = $this->get_curl_object(); $url = $this->get_connection_url('/update/extract'); // Return results as XML. $url->param('wt', 'xml'); // This will prevent solr from automatically making fields for every tika output. $url->param('uprefix', 'ignored_'); // Control how content is captured. This will keep our file content clean of non-important metadata. $url->param('captureAttr', 'true'); // Move the content to a field for indexing. $url->param('fmap.content', 'solr_filecontent'); // These are common fields that matches the standard *_point dynamic field and causes an error. $url->param('fmap.media_white_point', 'ignored_mwp'); $url->param('fmap.media_black_point', 'ignored_mbp'); // Copy each key to the url with literal. // We place in a temp name then copy back to the true field, which prevents errors or Tika overwriting common field names. foreach ($filedoc as $key => $value) { // This will take any fields from tika that match our schema and discard them, so they don't overwrite ours. $url->param('fmap.'.$key, 'ignored_'.$key); // Place data in a tmp field. $url->param('literal.mdltmp_'.$key, $value); // Then move to the final field. $url->param('fmap.mdltmp_'.$key, $key); } // This sets the true filename for Tika. $url->param('resource.name', $storedfile->get_filename()); // A giant block of code that is really just error checking around the curl request. try { // We have to post the file directly in binary data (not using multipart) to avoid // Solr bug SOLR-15039 which can cause incorrect data when you use multipart upload. // Note this loads the whole file into memory; see limit in file_is_indexable(). $result = $curl->post($url->out(false), $storedfile->get_content()); $code = $curl->get_errno(); $info = $curl->get_info(); // Now error handling. It is just informational, since we aren't tracking per file/doc results. if ($code != 0) { // This means an internal cURL error occurred error is in result. $message = 'Curl error '.$code.' while indexing file with document id '.$filedoc['id'].': '.$result.'.'; debugging($message, DEBUG_DEVELOPER); } else if (isset($info['http_code']) && ($info['http_code'] !== 200)) { // Unexpected HTTP response code. $message = 'Error while indexing file with document id '.$filedoc['id']; // Try to get error message out of msg or title if it exists. if (preg_match('|<str [^>]*name="msg"[^>]*>(.*?)</str>|i', $result, $matches)) { $message .= ': '.$matches[1]; } else if (preg_match('|<title[^>]*>([^>]*)</title>|i', $result, $matches)) { $message .= ': '.$matches[1]; } // This is a common error, happening whenever a file fails to index for any reason, so we will make it quieter. if (CLI_SCRIPT && !PHPUNIT_TEST) { mtrace($message); } } else { // Check for the expected status field. if (preg_match('|<int [^>]*name="status"[^>]*>(\d*)</int>|i', $result, $matches)) { // Now check for the expected status of 0, if not, error. if ((int)$matches[1] !== 0) { $message = 'Unexpected Solr status code '.(int)$matches[1]; $message .= ' while indexing file with document id '.$filedoc['id'].'.'; debugging($message, DEBUG_DEVELOPER); } else { // The document was successfully indexed. return; } } else { // We received an unprocessable response. $message = 'Unexpected Solr response while indexing file with document id '.$filedoc['id'].': '; $message .= strtok($result, "\n"); debugging($message, DEBUG_DEVELOPER); } } } catch (\Exception $e) { // There was an error, but we are not tracking per-file success, so we just continue on. debugging('Unknown exception while indexing file "'.$storedfile->get_filename().'".', DEBUG_DEVELOPER); } // If we get here, the document was not indexed due to an error. So we will index just the base info without the file. $filedoc['solr_fileindexstatus'] = document::INDEXED_FILE_ERROR; $this->add_solr_document($filedoc); } /** * Checks to see if a passed file is indexable. * * @param \stored_file $file The file to check * @return bool True if the file can be indexed */ protected function file_is_indexable($file) { if (!empty($this->config->maxindexfilekb) && ($file->get_filesize() > ($this->config->maxindexfilekb * 1024))) { // The file is too big to index. return false; } // Because we now load files into memory to index them in Solr, we also have to ensure that // we don't try to index anything bigger than the memory limit (less 100MB for safety). // Memory limit in cron is MEMORY_EXTRA which is usually 256 or 384MB but can be increased // in config, so this will allow files over 100MB to be indexed. $limit = ini_get('memory_limit'); if ($limit && $limit != -1) { $limitbytes = get_real_size($limit); if ($file->get_filesize() > $limitbytes) { return false; } } $mime = $file->get_mimetype(); if ($mime == 'application/vnd.moodle.backup') { // We don't index Moodle backup files. There is nothing usefully indexable in them. return false; } return true; } /** * Commits all pending changes. * * @return void */ protected function commit() { $this->get_search_client()->commit(); } /** * Do any area cleanup needed, and do anything to confirm contents. * * Return false to prevent the search area completed time and stats from being updated. * * @param \core_search\base $searcharea The search area that was complete * @param int $numdocs The number of documents that were added to the index * @param bool $fullindex True if a full index is being performed * @return bool True means that data is considered indexed */ public function area_index_complete($searcharea, $numdocs = 0, $fullindex = false) { $this->commit(); return true; } /** * Return true if file indexing is supported and enabled. False otherwise. * * @return bool */ public function file_indexing_enabled() { return (bool)$this->config->fileindexing; } /** * Deletes the specified document. * * @param string $id The document id to delete * @return void */ public function delete_by_id($id) { // We need to make sure we delete the item and all related files, which can be done with solr_filegroupingid. $this->get_search_client()->deleteByQuery('solr_filegroupingid:' . $id); $this->commit(); } /** * Delete all area's documents. * * @param string $areaid * @return void */ public function delete($areaid = null) { if ($areaid) { $this->get_search_client()->deleteByQuery('areaid:' . $areaid); } else { $this->get_search_client()->deleteByQuery('*:*'); } $this->commit(); } /** * Pings the Solr server using search_solr config * * @return true|string Returns true if all good or an error string. */ public function is_server_ready() { $configured = $this->is_server_configured(); if ($configured !== true) { return $configured; } // As part of the above we have already checked that we can contact the server. For pages // where performance is important, we skip doing a full schema check as well. if ($this->should_skip_schema_check()) { return true; } // Update schema if required/possible. $schemalatest = $this->check_latest_schema(); if ($schemalatest !== true) { return $schemalatest; } // Check that the schema is already set up. try { $schema = new schema($this); $schema->validate_setup(); } catch (\moodle_exception $e) { return $e->getMessage(); } return true; } /** * Is the solr server properly configured?. * * @return true|string Returns true if all good or an error string. */ public function is_server_configured() { if (empty($this->config->server_hostname) || empty($this->config->indexname)) { return 'No solr configuration found'; } if (!$client = $this->get_search_client(false)) { return get_string('engineserverstatus', 'search'); } try { if ($this->get_solr_major_version() < 4) { // Minimum solr 4.0. return get_string('minimumsolr4', 'search_solr'); } } catch (\SolrClientException $ex) { debugging('Solr client error: ' . html_to_text($ex->getMessage()), DEBUG_DEVELOPER); return get_string('engineserverstatus', 'search'); } catch (\SolrServerException $ex) { debugging('Solr server error: ' . html_to_text($ex->getMessage()), DEBUG_DEVELOPER); return get_string('engineserverstatus', 'search'); } return true; } /** * Returns the solr server major version. * * @return int */ public function get_solr_major_version() { if ($this->solrmajorversion !== null) { return $this->solrmajorversion; } // We should really ping first the server to see if the specified indexname is valid but // we want to minimise solr server requests as they are expensive. system() emits a warning // if it can not connect to the configured index in the configured server. $systemdata = @$this->get_search_client()->system(); $solrversion = $systemdata->getResponse()->offsetGet('lucene')->offsetGet('solr-spec-version'); $this->solrmajorversion = intval(substr($solrversion, 0, strpos($solrversion, '.'))); return $this->solrmajorversion; } /** * Checks if the PHP Solr extension is available. * * @return bool */ public function is_installed() { return function_exists('solr_get_version'); } /** * Returns the solr client instance. * * We don't reuse SolrClient if we are on libcurl 7.35.0, due to a bug in that version of curl. * * @throws \core_search\engine_exception * @param bool $triggerexception * @return \SolrClient */ protected function get_search_client($triggerexception = true) { global $CFG; // Type comparison as it is set to false if not available. if ($this->client !== null) { return $this->client; } $options = array( 'hostname' => $this->config->server_hostname, 'path' => '/solr/' . $this->config->indexname, 'login' => !empty($this->config->server_username) ? $this->config->server_username : '', 'password' => !empty($this->config->server_password) ? $this->config->server_password : '', 'port' => !empty($this->config->server_port) ? $this->config->server_port : '', 'secure' => !empty($this->config->secure) ? true : false, 'ssl_cert' => !empty($this->config->ssl_cert) ? $this->config->ssl_cert : '', 'ssl_key' => !empty($this->config->ssl_key) ? $this->config->ssl_key : '', 'ssl_keypassword' => !empty($this->config->ssl_keypassword) ? $this->config->ssl_keypassword : '', 'ssl_cainfo' => !empty($this->config->ssl_cainfo) ? $this->config->ssl_cainfo : '', 'ssl_capath' => !empty($this->config->ssl_capath) ? $this->config->ssl_capath : '', 'timeout' => !empty($this->config->server_timeout) ? $this->config->server_timeout : '30' ); if ($CFG->proxyhost && !is_proxybypass('http://' . $this->config->server_hostname . '/')) { $options['proxy_host'] = $CFG->proxyhost; if (!empty($CFG->proxyport)) { $options['proxy_port'] = $CFG->proxyport; } if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) { $options['proxy_login'] = $CFG->proxyuser; $options['proxy_password'] = $CFG->proxypassword; } } if (!class_exists('\SolrClient')) { throw new \core_search\engine_exception('enginenotinstalled', 'search', '', 'solr'); } $client = new \SolrClient($options); if ($client === false && $triggerexception) { throw new \core_search\engine_exception('engineserverstatus', 'search'); } if ($this->cacheclient) { $this->client = $client; } return $client; } /** * Returns a curl object for conntecting to solr. * * @return \curl */ public function get_curl_object() { if (!is_null($this->curl)) { return $this->curl; } // Connection to Solr is allowed to use 'localhost' and other potentially blocked hosts/ports. $this->curl = new \curl(['ignoresecurity' => true]); $options = array(); // Build the SSL options. Based on pecl-solr and general testing. if (!empty($this->config->secure)) { if (!empty($this->config->ssl_cert)) { $options['CURLOPT_SSLCERT'] = $this->config->ssl_cert; $options['CURLOPT_SSLCERTTYPE'] = 'PEM'; } if (!empty($this->config->ssl_key)) { $options['CURLOPT_SSLKEY'] = $this->config->ssl_key; $options['CURLOPT_SSLKEYTYPE'] = 'PEM'; } if (!empty($this->config->ssl_keypassword)) { $options['CURLOPT_KEYPASSWD'] = $this->config->ssl_keypassword; } if (!empty($this->config->ssl_cainfo)) { $options['CURLOPT_CAINFO'] = $this->config->ssl_cainfo; } if (!empty($this->config->ssl_capath)) { $options['CURLOPT_CAPATH'] = $this->config->ssl_capath; } } // Set timeout as for Solr client. $options['CURLOPT_TIMEOUT'] = !empty($this->config->server_timeout) ? $this->config->server_timeout : '30'; $this->curl->setopt($options); if (!empty($this->config->server_username) && !empty($this->config->server_password)) { $authorization = $this->config->server_username . ':' . $this->config->server_password; $this->curl->setHeader('Authorization: Basic ' . base64_encode($authorization)); } return $this->curl; } /** * Return a Moodle url object for the server connection. * * @param string $path The solr path to append. * @return \moodle_url */ public function get_connection_url($path) { // Must use the proper protocol, or SSL will fail. $protocol = !empty($this->config->secure) ? 'https' : 'http'; $url = $protocol . '://' . rtrim($this->config->server_hostname, '/'); if (!empty($this->config->server_port)) { $url .= ':' . $this->config->server_port; } $url .= '/solr/' . $this->config->indexname . '/' . ltrim($path, '/'); return new \moodle_url($url); } /** * Solr includes group support in the execute_query function. * * @return bool True */ public function supports_group_filtering() { return true; } protected function update_schema($oldversion, $newversion) { // Construct schema. $schema = new schema($this); $cansetup = $schema->can_setup_server(); if ($cansetup !== true) { return $cansetup; } switch ($newversion) { // This version just requires a setup call to add new fields. case 2017091700: $setup = true; break; // If we don't know about the schema version we might not have implemented the // change correctly, so return. default: return get_string('schemaversionunknown', 'search'); } if ($setup) { $schema->setup(); } return true; } /** * Solr supports sort by location within course contexts or below. * * @param \context $context Context that the user requested search from * @return array Array from order name => display text */ public function get_supported_orders(\context $context) { $orders = parent::get_supported_orders($context); // If not within a course, no other kind of sorting supported. $coursecontext = $context->get_course_context(false); if ($coursecontext) { // Within a course or activity/block, support sort by location. $orders['location'] = get_string('order_location', 'search', $context->get_context_name()); } return $orders; } /** * Solr supports search by user id. * * @return bool True */ public function supports_users() { return true; } /** * Solr supports adding documents in a batch. * * @return bool True */ public function supports_add_document_batch(): bool { return true; } /** * Solr supports deleting the index for a context. * * @param int $oldcontextid Context that has been deleted * @return bool True to indicate that any data was actually deleted * @throws \core_search\engine_exception */ public function delete_index_for_context(int $oldcontextid) { $client = $this->get_search_client(); try { $client->deleteByQuery('contextid:' . $oldcontextid); $client->commit(true); return true; } catch (\Exception $e) { throw new \core_search\engine_exception('error_solr', 'search_solr', '', $e->getMessage()); } } /** * Solr supports deleting the index for a course. * * @param int $oldcourseid * @return bool True to indicate that any data was actually deleted * @throws \core_search\engine_exception */ public function delete_index_for_course(int $oldcourseid) { $client = $this->get_search_client(); try { $client->deleteByQuery('courseid:' . $oldcourseid); $client->commit(true); return true; } catch (\Exception $e) { throw new \core_search\engine_exception('error_solr', 'search_solr', '', $e->getMessage()); } } /** * Checks if an alternate configuration has been defined. * * @return bool True if alternate configuration is available */ public function has_alternate_configuration(): bool { return !empty($this->config->alternateserver_hostname) && !empty($this->config->alternateindexname) && !empty($this->config->alternateserver_port); } } document.php 0000644 00000014651 15152167354 0007113 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Document representation. * * @package search_solr * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace search_solr; defined('MOODLE_INTERNAL') || die(); /** * Respresents a document to index. * * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class document extends \core_search\document { /** * Indicates the file contents were not indexed due to an error. */ const INDEXED_FILE_ERROR = -1; /** * Indicates the file contents were not indexed due filtering/settings. */ const INDEXED_FILE_FALSE = 0; /** * Indicates the file contents are indexed with the record. */ const INDEXED_FILE_TRUE = 1; /** * Any fields that are engine specifc. These are fields that are solely used by a seach engine plugin * for internal purposes. * * @var array */ protected static $enginefields = array( 'solr_filegroupingid' => array( 'type' => 'string', 'stored' => true, 'indexed' => true ), 'solr_fileid' => array( 'type' => 'string', 'stored' => true, 'indexed' => true ), 'solr_filecontenthash' => array( 'type' => 'string', 'stored' => true, 'indexed' => true ), // Stores the status of file indexing. 'solr_fileindexstatus' => array( 'type' => 'int', 'stored' => true, 'indexed' => true ), // Field to index, but not store, file contents. 'solr_filecontent' => array( 'type' => 'text', 'stored' => false, 'indexed' => true, 'mainquery' => true ) ); /** * Formats the timestamp according to the search engine needs. * * @param int $timestamp * @return string */ public static function format_time_for_engine($timestamp) { return gmdate(\search_solr\engine::DATE_FORMAT, $timestamp); } /** * Formats the timestamp according to the search engine needs. * * @param int $timestamp * @return string */ public static function format_string_for_engine($string) { // 2^15 default. We could convert this to a setting as is possible to // change the max in solr. return \core_text::str_max_bytes($string, 32766); } /** * Returns a timestamp from the value stored in the search engine. * * @param string $time * @return int */ public static function import_time_from_engine($time) { return strtotime($time); } /** * Overwritten to use markdown format as we use markdown for solr highlighting. * * @return int */ protected function get_text_format() { return FORMAT_HTML; } /** * Formats a text string coming from the search engine. * * @param string $text Text to format * @return string HTML text to be renderer */ protected function format_text($text) { // Since we allow output for highlighting, we need to encode html entities. // This ensures plaintext html chars don't become valid html. $out = s($text); $startcount = 0; $endcount = 0; // Remove end/start pairs that span a few common seperation characters. Allows us to highlight phrases instead of words. $regex = '|'.engine::HIGHLIGHT_END.'([ .,-]{0,3})'.engine::HIGHLIGHT_START.'|'; $out = preg_replace($regex, '$1', $out); // Now replace our start and end highlight markers. $out = str_replace(engine::HIGHLIGHT_START, '<span class="highlight">', $out, $startcount); $out = str_replace(engine::HIGHLIGHT_END, '</span>', $out, $endcount); // This makes sure any highlight tags are balanced, incase truncation or the highlight text contained our markers. while ($startcount > $endcount) { $out .= '</span>'; $endcount++; } while ($startcount < $endcount) { $out = '<span class="highlight">' . $out; $endcount++; } return parent::format_text($out); } /** * Apply any defaults to unset fields before export. Called after document building, but before export. * * Sub-classes of this should make sure to call parent::apply_defaults(). */ protected function apply_defaults() { parent::apply_defaults(); // We want to set the solr_filegroupingid to id if it isn't set. if (!isset($this->data['solr_filegroupingid'])) { $this->data['solr_filegroupingid'] = $this->data['id']; } } /** * Export the data for the given file in relation to this document. * * @param \stored_file $file The stored file we are talking about. * @return array */ public function export_file_for_engine($file) { $data = $this->export_for_engine(); // Content is index in the main document. unset($data['content']); unset($data['description1']); unset($data['description2']); // Going to append the fileid to give it a unique id. $data['id'] = $data['id'].'-solrfile'.$file->get_id(); $data['type'] = \core_search\manager::TYPE_FILE; $data['solr_fileid'] = $file->get_id(); $data['solr_filecontenthash'] = $file->get_contenthash(); $data['solr_fileindexstatus'] = self::INDEXED_FILE_TRUE; $data['title'] = $file->get_filename(); $data['modified'] = self::format_time_for_engine($file->get_timemodified()); return $data; } } event/report_viewed.php 0000644 00000003775 15152171676 0011303 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The report_security report viewed event. * * @package report_security * @copyright 2020 Brendan Heywood <brendan@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_security\event; defined('MOODLE_INTERNAL') || die(); /** * The report_loglive report viewed event class. * * @package report_security * @copyright 2020 Brendan Heywood <brendan@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_security'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the security check report."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/security/index.php'); } } event/user_report_viewed.php 0000644 00000005737 15152171676 0012341 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The report_completion user report viewed event. * * @package report_completion * @copyright 2014 onwards Ankit Agarwal<ankit.agrr@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_completion\event; defined('MOODLE_INTERNAL') || die(); /** * The report_completion user report viewed event class. * * @package report_completion * @since Moodle 2.7 * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_report_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventuserreportviewed', 'report_completion'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the user completion report for the user with id '$this->relateduserid'."; } /** * Return the legacy event log data. * * @return array */ protected function get_legacy_logdata() { $url = 'report/completion/user.php?id=' . $this->relateduserid . '&course=' . $this->courseid; return array($this->courseid, 'course', 'report completion', $url, $this->courseid); } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/completion/user.php', array('course' => $this->courseid, 'id' => $this->relateduserid)); } /** * custom validations. * * @throws \coding_exception when validation fails. * @return void */ protected function validate_data() { parent::validate_data(); if ($this->contextlevel != CONTEXT_COURSE) { throw new \coding_exception('Context level must be CONTEXT_COURSE.'); } if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } } output/translator.php 0000644 00000006030 15152206067 0011011 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * customlang specific renderers. * * @package tool_customlang * @copyright 2019 Moodle * @author Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_customlang\output; defined('MOODLE_INTERNAL') || die(); use renderable; use templatable; use renderer_base; use stdClass; /** * Class containing data for customlang translator page * * @copyright 2019 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class translator implements renderable, templatable { /** * @var tool_customlang_translator $translator object. */ private $translator; /** * Construct this renderable. * * @param tool_customlang_translator $translator The translator object. */ public function __construct(\tool_customlang_translator $translator) { $this->translator = $translator; } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $data->nostrings = $output->notification(get_string('nostringsfound', 'tool_customlang')); $data->formurl = $this->translator->handler; $data->currentpage = $this->translator->currentpage; $data->sesskey = sesskey(); $data->strings = []; if (!empty($this->translator->strings)) { $data->hasstrings = true; foreach ($this->translator->strings as $string) { // Find strings that use placeholders. if (preg_match('/\{\$a(->.+)?\}/', $string->master)) { $string->placeholderhelp = $output->help_icon('placeholder', 'tool_customlang', get_string('placeholderwarning', 'tool_customlang')); } if (!is_null($string->local) and $string->outdated) { $string->outdatedhelp = $output->help_icon('markinguptodate', 'tool_customlang'); $string->checkupdated = true; } if ($string->original !== $string->master) { $string->showoriginalvsmaster = true; } $string->local = s($string->local); $data->strings[] = $string; } } return $data; } } form/export.php 0000644 00000004470 15152206067 0007552 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Creates Formular for customlang file export * * @package tool_customlang * @copyright 2020 Thomas Wedekind <Thomas.Wedekind@univie.ac.at> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_customlang\form; use tool_customlang_utils; /** * Formular for customlang file export * * @copyright 2020 Thomas Wedekind <Thomas.Wedekind@univie.ac.at> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class export extends \moodleform { /** * Add elements to form */ public function definition() { $lng = $this->_customdata['lng']; $mform = $this->_form; $langdir = tool_customlang_utils::get_localpack_location($lng); // The export button only appears if a local lang is present. if (!check_dir_exists($langdir) || !count(glob("$langdir/*"))) { throw new \moodle_exception('nolocallang', 'tool_customlang'); } $langfiles = scandir($langdir); $fileoptions = []; foreach ($langfiles as $file) { if (substr($file, 0, 1) != '.') { $fileoptions[$file] = $file; } } $mform->addElement('hidden', 'lng', $lng); $mform->setType('lng', PARAM_LANG); $select = $mform->addElement('select', 'files', get_string('exportfilter', 'tool_customlang'), $fileoptions); $select->setMultiple(true); $mform->addRule('files', get_string('required'), 'required', null, 'client'); $mform->setDefault('files', $fileoptions); $this->add_action_buttons(true, get_string('export', 'tool_customlang')); } } form/import.php 0000644 00000004556 15152206067 0007550 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Upload a zip of custom lang php files. * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_customlang\form; use tool_customlang\local\importer; /** * Upload a zip/php of custom lang php files. * * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import extends \moodleform { /** * Form definition. */ public function definition() { $mform = $this->_form; $mform->addElement('header', 'settingsheader', get_string('import', 'tool_customlang')); $mform->addElement('hidden', 'lng'); $mform->setType('lng', PARAM_LANG); $mform->setDefault('lng', $this->_customdata['lng']); $filemanageroptions = array( 'accepted_types' => array('.php', '.zip'), 'maxbytes' => 0, 'maxfiles' => 1, 'subdirs' => 0 ); $mform->addElement('filepicker', 'pack', get_string('langpack', 'tool_customlang'), null, $filemanageroptions); $mform->addRule('pack', null, 'required'); $modes = [ importer::IMPORTALL => get_string('import_all', 'tool_customlang'), importer::IMPORTUPDATE => get_string('import_update', 'tool_customlang'), importer::IMPORTNEW => get_string('import_new', 'tool_customlang'), ]; $mform->addElement('select', 'importmode', get_string('import_mode', 'tool_customlang'), $modes); $mform->addElement('submit', 'importcustomstrings', get_string('importfile', 'tool_customlang')); } } local/mlang/langstring.php 0000644 00000016737 15152206067 0011637 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Language string based on David Mudrak langstring from local_amos. * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_customlang\local\mlang; use moodle_exception; use stdclass; /** * Class containing a lang string cleaned. * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Represents a single string */ class langstring { /** @var string identifier */ public $id = null; /** @var string */ public $text = ''; /** @var int the time stamp when this string was saved */ public $timemodified = null; /** @var bool is deleted */ public $deleted = false; /** @var stdclass extra information about the string */ public $extra = null; /** * Class constructor. * * @param string $id string identifier * @param string $text string text * @param int $timemodified * @param int $deleted * @param stdclass $extra */ public function __construct(string $id, string $text = '', int $timemodified = null, int $deleted = 0, stdclass $extra = null) { if (is_null($timemodified)) { $timemodified = time(); } $this->id = $id; $this->text = $text; $this->timemodified = $timemodified; $this->deleted = $deleted; $this->extra = $extra; } /** * Given a string text, returns it being formatted properly for storing in AMOS repository. * * Note: This method is taken directly from local_amos as it is highly tested and robust. * The Moodle 1.x part is keep on puspose to make it easier the copy paste from both codes. * This could change in the future when AMOS stop suporting the 1.x langstrings. * * We need to know for what branch the string should be prepared due to internal changes in * format required by get_string() * - for get_string() in Moodle 1.6 - 1.9 use $format == 1 * - for get_string() in Moodle 2.0 and higher use $format == 2 * * Typical usages of this methods: * $t = langstring::fix_syntax($t); // sanity new translations of 2.x strings * $t = langstring::fix_syntax($t, 1); // sanity legacy 1.x strings * $t = langstring::fix_syntax($t, 2, 1); // convert format of 1.x strings into 2.x * * Backward converting 2.x format into 1.x is not supported * * @param string $text string text to be fixed * @param int $format target get_string() format version * @param int $from which format version does the text come from, defaults to the same as $format * @return string */ public static function fix_syntax(string $text, int $format = 2, ?int $from = null): string { if (is_null($from)) { $from = $format; } // Common filter. $clean = trim($text); $search = [ // Remove \r if it is part of \r\n. '/\r(?=\n)/', // Control characters to be replaced with \n // LINE TABULATION, FORM FEED, CARRIAGE RETURN, END OF TRANSMISSION BLOCK, // END OF MEDIUM, SUBSTITUTE, BREAK PERMITTED HERE, NEXT LINE, START OF STRING, // STRING TERMINATOR and Unicode character categorys Zl and Zp. '/[\x{0B}-\r\x{17}\x{19}\x{1A}\x{82}\x{85}\x{98}\x{9C}\p{Zl}\p{Zp}]/u', // Control characters to be removed // NULL, ENQUIRY, ACKNOWLEDGE, BELL, SHIFT {OUT,IN}, DATA LINK ESCAPE, // DEVICE CONTROL {ONE,TWO,THREE,FOUR}, NEGATIVE ACKNOWLEDGE, SYNCHRONOUS IDLE, ESCAPE, // DELETE, PADDING CHARACTER, HIGH OCTET PRESET, NO BREAK HERE, INDEX, // {START,END} OF SELECTED AREA, CHARACTER TABULATION {SET,WITH JUSTIFICATION}, // LINE TABULATION SET, PARTIAL LINE {FORWARD,BACKWARD}, REVERSE LINE FEED, // SINGLE SHIFT {TWO,THREE}, DEVICE CONTROL STRING, PRIVATE USE {ONE,TWO}, // SET TRANSMIT STATE, MESSAGE WAITING, {START,END} OF GUARDED AREA, // {SINGLE {GRAPHIC,} CHARACTER,CONTROL SEQUENCE} INTRODUCER, OPERATING SYSTEM COMMAND, // PRIVACY MESSAGE, APPLICATION PROGRAM COMMAND, ZERO WIDTH {,NO-BREAK} SPACE, // REPLACEMENT CHARACTER. '/[\0\x{05}-\x{07}\x{0E}-\x{16}\x{1B}\x{7F}\x{80}\x{81}\x{83}\x{84}\x{86}-\x{93}\x{95}-\x{97}\x{99}-\x{9B}\x{9D}-\x{9F}\x{200B}\x{FEFF}\x{FFFD}]++/u', // Remove trailing whitespace at the end of lines in a multiline string. '/[ \t]+(?=\n)/', ]; $replace = [ '', "\n", '', '', ]; $clean = preg_replace($search, $replace, $clean); if (($format === 2) && ($from === 2)) { // Sanity translations of 2.x strings. $clean = preg_replace("/\n{3,}/", "\n\n\n", $clean); // Collapse runs of blank lines. } else if (($format === 2) && ($from === 1)) { // Convert 1.x string into 2.x format. $clean = preg_replace("/\n{3,}/", "\n\n\n", $clean); // Collapse runs of blank lines. $clean = preg_replace('/%+/', '%', $clean); // Collapse % characters. $clean = str_replace('\$', '@@@___XXX_ESCAPED_DOLLAR__@@@', $clean); // Remember for later. $clean = str_replace("\\", '', $clean); // Delete all slashes. $clean = preg_replace('/(^|[^{])\$a\b(\->[a-zA-Z0-9_]+)?/', '\\1{$a\\2}', $clean); // Wrap placeholders. $clean = str_replace('@@@___XXX_ESCAPED_DOLLAR__@@@', '$', $clean); $clean = str_replace('$', '$', $clean); } else if (($format === 1) && ($from === 1)) { // Sanity legacy 1.x strings. $clean = preg_replace("/\n{3,}/", "\n\n", $clean); // Collapse runs of blank lines. $clean = str_replace('\$', '@@@___XXX_ESCAPED_DOLLAR__@@@', $clean); $clean = str_replace("\\", '', $clean); // Delete all slashes. $clean = str_replace('$', '\$', $clean); // Escape all embedded variables. // Unescape placeholders: only $a and $a->something are allowed. All other $variables are left escaped. $clean = preg_replace('/\\\\\$a\b(\->[a-zA-Z0-9_]+)?/', '$a\\1', $clean); // Unescape placeholders. $clean = str_replace('@@@___XXX_ESCAPED_DOLLAR__@@@', '\$', $clean); $clean = str_replace('"', "\\\"", $clean); // Add slashes for ". $clean = preg_replace('/%+/', '%', $clean); // Collapse % characters. $clean = str_replace('%', '%%', $clean); // Duplicate %. } else { throw new moodle_exception('Unknown get_string() format version'); } return $clean; } } local/mlang/logstatus.php 0000644 00000005232 15152206067 0011500 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Language string based on David Mudrak langstring from local_amos. * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_customlang\local\mlang; use moodle_exception; use stdclass; /** * Class containing a lang string cleaned. * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Represents a single string */ class logstatus { /** @var langstring the current string */ public $langstring = null; /** @var string the component */ public $component = null; /** @var string the string ID */ public $stringid = null; /** @var string the original filename */ public $filename = null; /** @var int the error level */ public $errorlevel = null; /** @var string the message identifier */ private $message; /** * Class creator. * * @param string $message the message identifier to display * @param string $errorlevel the notice level * @param string|null $filename the filename of this log * @param string|null $component the component of this log * @param langstring|null $langstring the langstring of this log */ public function __construct(string $message, string $errorlevel, ?string $filename = null, ?string $component = null, ?langstring $langstring = null) { $this->filename = $filename; $this->component = $component; $this->langstring = $langstring; $this->message = $message; $this->errorlevel = $errorlevel; if ($langstring) { $this->stringid = $langstring->id; } } /** * Get the log message. * * @return string the log message. */ public function get_message(): string { return get_string($this->message, 'tool_customlang', $this); } } local/mlang/phpparser.php 0000644 00000021373 15152206067 0011463 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Mlang PHP based on David Mudrak phpparser for local_amos. * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_customlang\local\mlang; use coding_exception; use moodle_exception; /** * Parser of Moodle strings defined as associative array. * * Moodle core just includes this file format directly as normal PHP code. However * for security reasons, we must not do this for files uploaded by anonymous users. * This parser reconstructs the associative $string array without actually including * the file. */ class phpparser { /** @var holds the singleton instance of self */ private static $instance = null; /** * Prevents direct creation of object */ private function __construct() { } /** * Prevent from cloning the instance */ public function __clone() { throw new coding_exception('Cloning os singleton is not allowed'); } /** * Get the singleton instance fo this class * * @return phpparser singleton instance of phpparser */ public static function get_instance(): phpparser { if (is_null(self::$instance)) { self::$instance = new phpparser(); } return self::$instance; } /** * Parses the given data in Moodle PHP string format * * Note: This method is adapted from local_amos as it is highly tested and robust. * The priority is keeping it similar to the original one to make it easier to mantain. * * @param string $data definition of the associative array * @param int $format the data format on the input, defaults to the one used since 2.0 * @return langstring[] array of langstrings of this file */ public function parse(string $data, int $format = 2): array { $result = []; $strings = $this->extract_strings($data); foreach ($strings as $id => $text) { $cleaned = clean_param($id, PARAM_STRINGID); if ($cleaned !== $id) { continue; } $text = langstring::fix_syntax($text, 2, $format); $result[] = new langstring($id, $text); } return $result; } /** * Low level parsing method * * Note: This method is adapted from local_amos as it is highly tested and robust. * The priority is keeping it similar to the original one to make it easier to mantain. * * @param string $data * @return string[] the data strings */ protected function extract_strings(string $data): array { $strings = []; // To be returned. if (empty($data)) { return $strings; } // Tokenize data - we expect valid PHP code. $tokens = token_get_all($data); // Get rid of all non-relevant tokens. $rubbish = [T_WHITESPACE, T_INLINE_HTML, T_COMMENT, T_DOC_COMMENT, T_OPEN_TAG, T_CLOSE_TAG]; foreach ($tokens as $i => $token) { if (is_array($token)) { if (in_array($token[0], $rubbish)) { unset($tokens[$i]); } } } $id = null; $text = null; $line = 0; $expect = 'STRING_VAR'; // The first expected token is '$string'. // Iterate over tokens and look for valid $string array assignment patterns. foreach ($tokens as $token) { $foundtype = null; $founddata = null; if (is_array($token)) { $foundtype = $token[0]; $founddata = $token[1]; if (!empty($token[2])) { $line = $token[2]; } } else { $foundtype = 'char'; $founddata = $token; } if ($expect == 'STRING_VAR') { if ($foundtype === T_VARIABLE and $founddata === '$string') { $expect = 'LEFT_BRACKET'; continue; } else { // Allow other code at the global level. continue; } } if ($expect == 'LEFT_BRACKET') { if ($foundtype === 'char' and $founddata === '[') { $expect = 'STRING_ID'; continue; } else { throw new moodle_exception('Parsing error. Expected character [ at line '.$line); } } if ($expect == 'STRING_ID') { if ($foundtype === T_CONSTANT_ENCAPSED_STRING) { $id = $this->decapsulate($founddata); $expect = 'RIGHT_BRACKET'; continue; } else { throw new moodle_exception('Parsing error. Expected T_CONSTANT_ENCAPSED_STRING array key at line '.$line); } } if ($expect == 'RIGHT_BRACKET') { if ($foundtype === 'char' and $founddata === ']') { $expect = 'ASSIGNMENT'; continue; } else { throw new moodle_exception('Parsing error. Expected character ] at line '.$line); } } if ($expect == 'ASSIGNMENT') { if ($foundtype === 'char' and $founddata === '=') { $expect = 'STRING_TEXT'; continue; } else { throw new moodle_exception('Parsing error. Expected character = at line '.$line); } } if ($expect == 'STRING_TEXT') { if ($foundtype === T_CONSTANT_ENCAPSED_STRING) { $text = $this->decapsulate($founddata); $expect = 'SEMICOLON'; continue; } else { throw new moodle_exception( 'Parsing error. Expected T_CONSTANT_ENCAPSED_STRING array item value at line '.$line ); } } if ($expect == 'SEMICOLON') { if (is_null($id) or is_null($text)) { throw new moodle_exception('Parsing error. NULL string id or value at line '.$line); } if ($foundtype === 'char' and $founddata === ';') { if (!empty($id)) { $strings[$id] = $text; } $id = null; $text = null; $expect = 'STRING_VAR'; continue; } else { throw new moodle_exception('Parsing error. Expected character ; at line '.$line); } } } return $strings; } /** * Given one T_CONSTANT_ENCAPSED_STRING, return its value without quotes * * Also processes escaped quotes inside the text. * * Note: This method is taken directly from local_amos as it is highly tested and robust. * * @param string $text value obtained by token_get_all() * @return string value without quotes */ protected function decapsulate(string $text): string { if (strlen($text) < 2) { throw new moodle_exception('Parsing error. Expected T_CONSTANT_ENCAPSED_STRING in decapsulate()'); } if (substr($text, 0, 1) == "'" and substr($text, -1) == "'") { // Single quoted string. $text = trim($text, "'"); $text = str_replace("\'", "'", $text); $text = str_replace('\\\\', '\\', $text); return $text; } else if (substr($text, 0, 1) == '"' and substr($text, -1) == '"') { // Double quoted string. $text = trim($text, '"'); $text = str_replace('\"', '"', $text); $text = str_replace('\\\\', '\\', $text); return $text; } else { throw new moodle_exception( 'Parsing error. Unexpected quotation in T_CONSTANT_ENCAPSED_STRING in decapsulate(): '.$text ); } } } local/importer.php 0000644 00000020611 15152206067 0010214 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Custom lang importer. * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_customlang\local; use tool_customlang\local\mlang\phpparser; use tool_customlang\local\mlang\logstatus; use tool_customlang\local\mlang\langstring; use core\output\notification; use stored_file; use coding_exception; use moodle_exception; use core_component; use stdClass; /** * Class containing tha custom lang importer * * @package tool_customlang * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class importer { /** @var int imports will only create new customizations */ public const IMPORTNEW = 1; /** @var int imports will only update the current customizations */ public const IMPORTUPDATE = 2; /** @var int imports all strings */ public const IMPORTALL = 3; /** * @var string the language name */ protected $lng; /** * @var int the importation mode (new, update, all) */ protected $importmode; /** * @var string request folder path */ private $folder; /** * @var array import log messages */ private $log; /** * Constructor for the importer class. * * @param string $lng the current language to import. * @param int $importmode the import method (IMPORTALL, IMPORTNEW, IMPORTUPDATE). */ public function __construct(string $lng, int $importmode = self::IMPORTALL) { $this->lng = $lng; $this->importmode = $importmode; $this->log = []; } /** * Returns the last parse log. * * @return logstatus[] mlang logstatus with the messages */ public function get_log(): array { return $this->log; } /** * Import customlang files. * * @param stored_file[] $files array of files to import */ public function import(array $files): void { // Create a temporal folder to store the files. $this->folder = make_request_directory(false); $langfiles = $this->deploy_files($files); $this->process_files($langfiles); } /** * Deploy all files into a request folder. * * @param stored_file[] $files array of files to deploy * @return string[] of file paths */ private function deploy_files(array $files): array { $result = []; // Desploy all files. foreach ($files as $file) { if ($file->get_mimetype() == 'application/zip') { $result = array_merge($result, $this->unzip_file($file)); } else { $path = $this->folder.'/'.$file->get_filename(); $file->copy_content_to($path); $result = array_merge($result, [$path]); } } return $result; } /** * Unzip a file into the request folder. * * @param stored_file $file the zip file to unzip * @return string[] of zip content paths */ private function unzip_file(stored_file $file): array { $fp = get_file_packer('application/zip'); $zipcontents = $fp->extract_to_pathname($file, $this->folder); if (!$zipcontents) { throw new moodle_exception("Error Unzipping file", 1); } $result = []; foreach ($zipcontents as $contentname => $success) { if ($success) { $result[] = $this->folder.'/'.$contentname; } } return $result; } /** * Import strings from a list of langfiles. * * @param string[] $langfiles an array with file paths */ private function process_files(array $langfiles): void { $parser = phpparser::get_instance(); foreach ($langfiles as $filepath) { $component = $this->component_from_filepath($filepath); if ($component) { $strings = $parser->parse(file_get_contents($filepath)); $this->import_strings($strings, $component); } } } /** * Try to get the component from a filepath. * * @param string $filepath the filepath * @return stdCalss|null the DB record of that component */ private function component_from_filepath(string $filepath) { global $DB; // Get component from filename. $pathparts = pathinfo($filepath); if (empty($pathparts['filename'])) { throw new coding_exception("Cannot get filename from $filepath", 1); } $filename = $pathparts['filename']; $normalized = core_component::normalize_component($filename); if (count($normalized) == 1 || empty($normalized[1])) { $componentname = $normalized[0]; } else { $componentname = implode('_', $normalized); } $result = $DB->get_record('tool_customlang_components', ['name' => $componentname]); if (!$result) { $this->log[] = new logstatus('notice_missingcomponent', notification::NOTIFY_ERROR, null, $componentname); return null; } return $result; } /** * Import an array of strings into the customlang tables. * * @param langstring[] $strings the langstring to set * @param stdClass $component the target component */ private function import_strings(array $strings, stdClass $component): void { global $DB; foreach ($strings as $newstring) { // Check current DB entry. $customlang = $DB->get_record('tool_customlang', [ 'componentid' => $component->id, 'stringid' => $newstring->id, 'lang' => $this->lng, ]); if (!$customlang) { $customlang = null; } if ($this->can_save_string($customlang, $newstring, $component)) { $customlang->local = $newstring->text; $customlang->timecustomized = $newstring->timemodified; $customlang->outdated = 0; $customlang->modified = 1; $DB->update_record('tool_customlang', $customlang); } } } /** * Determine if a specific string can be saved based on the current importmode. * * @param stdClass $customlang customlang original record * @param langstring $newstring the new strign to store * @param stdClass $component the component target * @return bool if the string can be stored */ private function can_save_string(?stdClass $customlang, langstring $newstring, stdClass $component): bool { $result = false; $message = 'notice_success'; if (empty($customlang)) { $message = 'notice_inexitentstring'; $this->log[] = new logstatus($message, notification::NOTIFY_ERROR, null, $component->name, $newstring); return $result; } switch ($this->importmode) { case self::IMPORTNEW: $result = empty($customlang->local); $warningmessage = 'notice_ignoreupdate'; break; case self::IMPORTUPDATE: $result = !empty($customlang->local); $warningmessage = 'notice_ignorenew'; break; case self::IMPORTALL: $result = true; break; } if ($result) { $errorlevel = notification::NOTIFY_SUCCESS; } else { $errorlevel = notification::NOTIFY_ERROR; $message = $warningmessage; } $this->log[] = new logstatus($message, $errorlevel, null, $component->name, $newstring); return $result; } } browser/contentbank_browser_context_system.php 0000644 00000004757 15152206206 0016175 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Utility class for browsing of content bank files in the system context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the system context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_system extends contentbank_browser { /** * Constructor. * * @param \context_system $context The current context */ public function __construct(\context_system $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The expected child context in the system context level is the course category context. return [\CONTEXT_COURSECAT]; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the system // context level all over the site. // The content from the system context level should be available to: // * Every user that has a capability to access the 'general' content. return has_capability('repository/contentbank:accessgeneralcontent', $this->context); } } browser/contentbank_browser.php 0000644 00000015416 15152206206 0013017 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Utility class for browsing of content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Base class for the content bank browsers. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class contentbank_browser { /** @var \context The current context. */ protected $context; /** * Get all content nodes in the current context which can be viewed/accessed by the user. * * @return array[] The array containing all nodes which can be viewed/accessed by the user in the current context */ public function get_content(): array { return array_merge($this->get_context_folders(), $this->get_contentbank_content()); } /** * Generate the full navigation to the current node. * * @return array[] The array containing the path to each node in the navigation. * Each navigation node is an array with keys: name, path. */ public function get_navigation(): array { // Get the current navigation node. $currentnavigationnode = \repository_contentbank\helper::create_navigation_node($this->context); $navigationnodes = [$currentnavigationnode]; // Get the parent content bank browser. $parent = $this->get_parent(); // Prepend parent navigation node in the navigation nodes array until there is no existing parent. while ($parent !== null) { $parentnavigationnode = \repository_contentbank\helper::create_navigation_node($parent->context); array_unshift($navigationnodes, $parentnavigationnode); $parent = $parent->get_parent(); } return $navigationnodes; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ abstract public function can_access_content(): bool; /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ abstract protected function allowed_child_context_levels(): array; /** * Get the relevant child contexts. * * @return \context[] The array containing the relevant, next-level children contexts */ protected function get_child_contexts(): array { global $DB; if (empty($allowedcontextlevels = $this->allowed_child_context_levels())) { // Early return if there aren't any defined child context levels. return []; } list($contextlevelsql, $params) = $DB->get_in_or_equal($allowedcontextlevels, SQL_PARAMS_NAMED); $pathsql = $DB->sql_like('path', ':path', false, false); $select = "contextlevel {$contextlevelsql} AND {$pathsql} AND depth = :depth"; $params['path'] = "{$this->context->path}/%"; $params['depth'] = $this->context->depth + 1; $childcontexts = $DB->get_records_select('context', $select, $params); return array_map(function($childcontext) { return \context::instance_by_id($childcontext->id); }, $childcontexts); } /** * Get the content bank browser class of the parent context. Currently used to generate the navigation path. * * @return contentbank_browser|null The content bank browser of the parent context */ private function get_parent(): ?self { if ($parentcontext = $this->context->get_parent_context()) { return \repository_contentbank\helper::get_contentbank_browser($parentcontext); } return null; } /** * Generate folder nodes for the relevant child contexts which can be accessed/viewed by the user. * * @return array[] The array containing the context folder nodes where each folder node is an array with keys: * title, datemodified, datecreated, path, thumbnail, children. */ private function get_context_folders(): array { // Get all relevant child contexts. $children = $this->get_child_contexts(); // Return all child context folder nodes which can be accessed by the user following the defined conditions // in can_access_content(). return array_reduce($children, function ($list, $child) { $browser = \repository_contentbank\helper::get_contentbank_browser($child); if ($browser->can_access_content()) { $name = $child->get_context_name(false); $path = base64_encode(json_encode(['contextid' => $child->id])); $list[] = \repository_contentbank\helper::create_context_folder_node($name, $path); } return $list; }, []); } /** * Generate nodes for the content bank content in the current context which can be accessed/viewed by the user. * * @return array[] The array containing the content nodes where each content node is an array with keys: * shorttitle, title, datemodified, datecreated, author, license, isref, source, icon, thumbnail. */ private function get_contentbank_content(): array { $cb = new \core_contentbank\contentbank(); // Get all content bank files in the current context. $contents = $cb->search_contents(null, $this->context->id); // Return all content bank content nodes from the current context which can be accessed by the user following // the defined conditions in can_access_content(). return array_reduce($contents, function($list, $content) { if ($this->can_access_content() && $contentnode = \repository_contentbank\helper::create_contentbank_content_node($content)) { $list[] = $contentnode; } return $list; }, []); } } browser/contentbank_browser_context_course.php 0000644 00000004772 15152206206 0016146 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Utility class for browsing of content bank files in the course context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the course context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_course extends contentbank_browser { /** * Constructor. * * @param \context_course $context The current context */ public function __construct(\context_course $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The course context is the last relevant context level, therefore child context levels are not being returned. return []; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the course // context level all over the site. // The content from the course context level should be available to: // * Every user which has capability to access the content of a given course. return has_capability('repository/contentbank:accesscoursecontent', $this->context); } } browser/contentbank_browser_context_coursecat.php 0000644 00000006452 15152206206 0016633 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Utility class for browsing of content bank files in the course category context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank\browser; /** * Represents the content bank browser in the course category context. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_browser_context_coursecat extends contentbank_browser { /** * Constructor. * * @param \context_coursecat $context The current context */ public function __construct(\context_coursecat $context) { $this->context = $context; } /** * Define the allowed child context levels. * * @return int[] The array containing the relevant child context levels */ protected function allowed_child_context_levels(): array { // The expected child contexts in the course category context level are the course category context // (ex. subcategories) and the course context. return [\CONTEXT_COURSECAT, \CONTEXT_COURSE]; } /** * The required condition to enable the user to view/access the content bank content in this context. * * @return bool Whether the user can view/access the content bank content in the context */ public function can_access_content(): bool { // When the following conditions are met, the user would be able to share the content created in the course // category context level all over the site. // The content from the course category context level should be available to either: // * Every user which has a capability to access the 'general' content and has capability to access the // content of any child course of the given course category. // * Users that have capability to access content at a course category context level. if (has_capability('repository/contentbank:accesscoursecategorycontent', $this->context)) { return true; } $canaccesschildcontent = false; foreach ($this->get_child_contexts() as $childcontext) { $browser = \repository_contentbank\helper::get_contentbank_browser($childcontext); if ($canaccesschildcontent = $browser->can_access_content()) { break; } } return $canaccesschildcontent && has_capability('repository/contentbank:accessgeneralcontent', $this->context); } } contentbank_search.php 0000644 00000005203 15152206206 0011107 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Utility class for searching of content bank files. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_contentbank; /** * Represents the content bank search related functionality. * * @package repository_contentbank * @copyright 2020 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_search { /** * Generate and return content nodes for all content bank files that match the search criteria * and can be viewed/accessed by the user. * * @param string $search The search string * @return array[] The array containing all content file nodes that match the search criteria. Each content node is * an array with keys: shorttitle, title, datemodified, datecreated, author, license, isref, source, * icon, thumbnail. */ public static function get_search_contents(string $search): array { $contentbank = new \core_contentbank\contentbank(); // Return all content bank content that matches the search criteria and can be viewed/accessed by the user. $contents = $contentbank->search_contents($search); return array_reduce($contents, function($list, $content) { $contentcontext = \context::instance_by_id($content->get_content()->contextid); $browser = \repository_contentbank\helper::get_contentbank_browser($contentcontext); // If the user can access the content and content node can be created, add the node into the // search results list. if ($browser->can_access_content() && $contentnode = \repository_contentbank\helper::create_contentbank_content_node($content)) { $list[] = $contentnode; } return $list; }, []); } } completion/custom_completion.php 0000644 00000005521 15152217651 0013201 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace mod_resource\completion; use core_completion\activity_custom_completion; /** * Activity custom completion subclass for the resource. * * Class for defining mod_resource's custom completion rules and fetching the completion statuses * of the custom completion rules for a given resource instance and a user. * * @package mod_resource * @copyright 2021 Huong Nguyen <huongn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom_completion extends activity_custom_completion { /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ public function get_state(string $rule): int { return COMPLETION_UNKNOWN; } /** * Fetch the list of custom completion rules that this module defines. * * @return array */ public static function get_defined_custom_rules(): array { // This activity/resource do not have any custom rules. return []; } /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ public function get_custom_rule_descriptions(): array { // This activity/resource do not have any custom rule descriptions. return []; } /** * Show the manual completion or not regardless of the course's showcompletionconditions setting. * * @return bool */ public function manual_completion_always_shown(): bool { $display = $this->cm->customdata['display'] ?? null; $displaytypes = [ RESOURCELIB_DISPLAY_NEW, RESOURCELIB_DISPLAY_OPEN, RESOURCELIB_DISPLAY_DOWNLOAD, RESOURCELIB_DISPLAY_POPUP ]; return in_array($display, $displaytypes); } /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ public function get_sort_order(): array { // This module only supports manual completion. return []; } } content/exporter.php 0000644 00000004012 15152217651 0010601 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Content export definition. * * @package mod_resource * @copyright 2020 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_resource\content; use core\content\export\exportable_items\exportable_filearea; use core\content\export\exporters\abstract_mod_exporter; /** * A class which assists a component to export content. * * @copyright 2020 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class exporter extends abstract_mod_exporter { /** * Get the exportable items for mod_resource. * * @param bool $includeuserdata Whether to include user data, in addition to shared content. * @return \core\content\export\exportable_item[] */ public function get_exportables(bool $includeuserdata = false): array { $contentitems = []; $contentitems[] = new exportable_filearea( $this->get_context(), $this->get_component(), get_string('resourcecontent', 'mod_resource'), // The files held in mod_resource are stored in the 'content' filearea, under itemid 0. 'content', 0, // The itemid is used in the URL when accessing. 0 ); return $contentitems; } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 1.87 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�