MDL-48391 tool_uploadcourse: access checks for configuring enrolments.
[moodle.git] / admin / tool / uploadcourse / classes / helper.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * File containing the helper class.
19  *
20  * @package    tool_uploadcourse
21  * @copyright  2013 Frédéric Massart
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
26 require_once($CFG->dirroot . '/cache/lib.php');
27 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
28 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
30 /**
31  * Class containing a set of helpers.
32  *
33  * @package    tool_uploadcourse
34  * @copyright  2013 Frédéric Massart
35  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36  */
37 class tool_uploadcourse_helper {
39     /**
40      * Generate a shortname based on a template.
41      *
42      * @param array|object $data course data.
43      * @param string $templateshortname template of shortname.
44      * @return null|string shortname based on the template, or null when an error occured.
45      */
46     public static function generate_shortname($data, $templateshortname) {
47         if (empty($templateshortname) && !is_numeric($templateshortname)) {
48             return null;
49         }
50         if (strpos($templateshortname, '%') === false) {
51             return $templateshortname;
52         }
54         $course = (object) $data;
55         $fullname   = isset($course->fullname) ? $course->fullname : '';
56         $idnumber   = isset($course->idnumber) ? $course->idnumber  : '';
58         $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
59         $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
61         if (!is_null($result)) {
62             $result = clean_param($result, PARAM_TEXT);
63         }
65         if (empty($result) && !is_numeric($result)) {
66             $result = null;
67         }
69         return $result;
70     }
72     /**
73      * Callback used when generating a shortname based on a template.
74      *
75      * @param string $fullname full name.
76      * @param string $idnumber ID number.
77      * @param array $block result from preg_replace_callback.
78      * @return string
79      */
80     public static function generate_shortname_callback($fullname, $idnumber, $block) {
81         switch ($block[3]) {
82             case 'f':
83                 $repl = $fullname;
84                 break;
85             case 'i':
86                 $repl = $idnumber;
87                 break;
88             default:
89                 return $block[0];
90         }
92         switch ($block[1]) {
93             case '+':
94                 $repl = core_text::strtoupper($repl);
95                 break;
96             case '-':
97                 $repl = core_text::strtolower($repl);
98                 break;
99             case '~':
100                 $repl = core_text::strtotitle($repl);
101                 break;
102         }
104         if (!empty($block[2])) {
105             $repl = core_text::substr($repl, 0, $block[2]);
106         }
108         return $repl;
109     }
111     /**
112      * Return the available course formats.
113      *
114      * @return array
115      */
116     public static function get_course_formats() {
117         return array_keys(core_component::get_plugin_list('format'));
118     }
120     /**
121      * Extract enrolment data from passed data.
122      *
123      * Constructs an array of methods, and their options:
124      * array(
125      *     'method1' => array(
126      *         'option1' => value,
127      *         'option2' => value
128      *     ),
129      *     'method2' => array(
130      *         'option1' => value,
131      *         'option2' => value
132      *     )
133      * )
134      *
135      * @param array $data data to extract the enrolment data from.
136      * @return array
137      */
138     public static function get_enrolment_data($data) {
139         $enrolmethods = array();
140         $enroloptions = array();
141         foreach ($data as $field => $value) {
143             // Enrolmnent data.
144             $matches = array();
145             if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) {
146                 $key = $matches[1];
147                 if (!isset($enroloptions[$key])) {
148                     $enroloptions[$key] = array();
149                 }
150                 if (empty($matches[3])) {
151                     $enrolmethods[$key] = $value;
152                 } else {
153                     $enroloptions[$key][$matches[3]] = $value;
154                 }
155             }
156         }
158         // Combining enrolment methods and their options in a single array.
159         $enrolmentdata = array();
160         if (!empty($enrolmethods)) {
161             $enrolmentplugins = self::get_enrolment_plugins();
162             foreach ($enrolmethods as $key => $method) {
163                 if (!array_key_exists($method, $enrolmentplugins)) {
164                     // Error!
165                     continue;
166                 }
167                 $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
168             }
169         }
170         return $enrolmentdata;
171     }
173     /**
174      * Return the enrolment plugins.
175      *
176      * The result is cached for faster execution.
177      *
178      * @return enrol_plugin[]
179      */
180     public static function get_enrolment_plugins() {
181         $cache = cache::make('tool_uploadcourse', 'helper');
182         if (($enrol = $cache->get('enrol')) === false) {
183             $enrol = enrol_get_plugins(false);
184             $cache->set('enrol', $enrol);
185         }
186         return $enrol;
187     }
189     /**
190      * Get the restore content tempdir.
191      *
192      * The tempdir is the sub directory in which the backup has been extracted.
193      *
194      * This caches the result for better performance, but $CFG->keeptempdirectoriesonbackup
195      * needs to be enabled, otherwise the cache is ignored.
196      *
197      * @param string $backupfile path to a backup file.
198      * @param string $shortname shortname of a course.
199      * @param array $errors will be populated with errors found.
200      * @return string|false false when the backup couldn't retrieved.
201      */
202     public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
203         global $CFG, $DB, $USER;
205         $cachekey = null;
206         if (!empty($backupfile)) {
207             $backupfile = realpath($backupfile);
208             if (empty($backupfile) || !is_readable($backupfile)) {
209                 $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
210                 return false;
211             }
212             $cachekey = 'backup_path:' . $backupfile;
213         } else if (!empty($shortname) || is_numeric($shortname)) {
214             $cachekey = 'backup_sn:' . $shortname;
215         }
217         if (empty($cachekey)) {
218             return false;
219         }
221         // If $CFG->keeptempdirectoriesonbackup is not set to true, any restore happening would
222         // automatically delete the backup directory... causing the cache to return an unexisting directory.
223         $usecache = !empty($CFG->keeptempdirectoriesonbackup);
224         if ($usecache) {
225             $cache = cache::make('tool_uploadcourse', 'helper');
226         }
228         // If we don't use the cache, or if we do and not set, or the directory doesn't exist any more.
229         if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir(get_backup_temp_directory($backupid)))) {
231             // Use null instead of false because it would consider that the cache key has not been set.
232             $backupid = null;
234             if (!empty($backupfile)) {
235                 // Extracting the backup file.
236                 $packer = get_file_packer('application/vnd.moodle.backup');
237                 $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
238                 $path = make_backup_temp_directory($backupid, false);
239                 $result = $packer->extract_to_pathname($backupfile, $path);
240                 if (!$result) {
241                     $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
242                 }
243             } else if (!empty($shortname) || is_numeric($shortname)) {
244                 // Creating restore from an existing course.
245                 $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
246                 if (!empty($courseid)) {
247                     $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
248                         backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
249                     $bc->execute_plan();
250                     $backupid = $bc->get_backupid();
251                     $bc->destroy();
252                 } else {
253                     $errors['coursetorestorefromdoesnotexist'] =
254                         new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
255                 }
256             }
258             if ($usecache) {
259                 $cache->set($cachekey, $backupid);
260             }
261         }
263         if ($backupid === null) {
264             $backupid = false;
265         }
266         return $backupid;
267     }
269     /**
270      * Return the role IDs.
271      *
272      * The result is cached for faster execution.
273      *
274      * @return array
275      */
276     public static function get_role_ids() {
277         $cache = cache::make('tool_uploadcourse', 'helper');
278         if (($roles = $cache->get('roles')) === false) {
279             $roles = array();
280             $rolesraw = get_all_roles();
281             foreach ($rolesraw as $role) {
282                 $roles[$role->shortname] = $role->id;
283             }
284             $cache->set('roles', $roles);
285         }
286         return $roles;
287     }
289     /**
290      * Helper to detect how many sections a course with a given shortname has.
291      *
292      * @param string $shortname shortname of a course to count sections from.
293      * @return integer count of sections.
294      */
295     public static function get_coursesection_count($shortname) {
296         global $DB;
297         if (!empty($shortname) || is_numeric($shortname)) {
298             // Creating restore from an existing course.
299             $course = $DB->get_record('course', array('shortname' => $shortname));
300         }
301         if (!empty($course)) {
302             $courseformat = course_get_format($course);
303             return $courseformat->get_last_section_number();
304         }
305         return 0;
306     }
308     /**
309      * Get the role renaming data from the passed data.
310      *
311      * @param array $data data to extract the names from.
312      * @param array $errors will be populated with errors found.
313      * @return array where the key is the role_<id>, the value is the new name.
314      */
315     public static function get_role_names($data, &$errors = array()) {
316         $rolenames = array();
317         $rolesids = self::get_role_ids();
318         $invalidroles = array();
319         foreach ($data as $field => $value) {
321             $matches = array();
322             if (preg_match('/^role_(.+)?$/', $field, $matches)) {
323                 if (!isset($rolesids[$matches[1]])) {
324                     $invalidroles[] = $matches[1];
325                     continue;
326                 }
327                 $rolenames['role_' . $rolesids[$matches[1]]] = $value;
328             }
330         }
332         if (!empty($invalidroles)) {
333             $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
334         }
336         // Roles names.
337         return $rolenames;
338     }
340     /**
341      * Return array of all custom course fields indexed by their shortname
342      *
343      * @return \core_customfield\field_controller[]
344      */
345     public static function get_custom_course_fields(): array {
346         $result = [];
348         $fields = \core_course\customfield\course_handler::create()->get_fields();
349         foreach ($fields as $field) {
350             $result[$field->get('shortname')] = $field;
351         }
353         return $result;
354     }
356     /**
357      * Return array of custom field element names
358      *
359      * @return string[]
360      */
361     public static function get_custom_course_field_names(): array {
362         $result = [];
364         $fields = self::get_custom_course_fields();
365         foreach ($fields as $field) {
366             $controller = \core_customfield\data_controller::create(0, null, $field);
367             $result[] = $controller->get_form_element_name();
368         }
370         return $result;
371     }
373     /**
374      * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
375      *
376      * @param array $data
377      * @param array $defaults
378      * @param context $context
379      * @param array $errors Will be populated with any errors
380      * @return array
381      */
382     public static function get_custom_course_field_data(array $data, array $defaults, context $context,
383             array &$errors = []): array {
385         $fields = self::get_custom_course_fields();
386         $result = [];
388         $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
390         foreach ($data as $name => $originalvalue) {
391             if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
392                     && isset($fields[$matches['name']])) {
394                 $fieldname = $matches['name'];
395                 $field = $fields[$fieldname];
397                 // Skip field if it's locked and user doesn't have capability to change locked fields.
398                 if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
399                     continue;
400                 }
402                 // Create field data controller.
403                 $controller = \core_customfield\data_controller::create(0, null, $field);
404                 $controller->set('id', 1);
406                 $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
407                 $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
409                 // If we initially had a value, but now don't, then reset it to the default.
410                 if (!empty($originalvalue) && empty($value)) {
411                     $value = $defaultvalue;
412                 }
414                 // Validate data with controller.
415                 $fieldformdata = [$controller->get_form_element_name() => $value];
416                 $validationerrors = $controller->instance_form_validation($fieldformdata, []);
417                 if (count($validationerrors) > 0) {
418                     $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
419                         $field->get_formatted_name());
421                     continue;
422                 }
424                 $controller->set($controller->datafield(), $value);
426                 // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
427                 $instance = new stdClass();
428                 $controller->instance_form_before_set_data($instance);
430                 $result = array_merge($result, (array) $instance);
431             }
432         }
434         return $result;
435     }
437     /**
438      * Helper to increment an ID number.
439      *
440      * This first checks if the ID number is in use.
441      *
442      * @param string $idnumber ID number to increment.
443      * @return string new ID number.
444      */
445     public static function increment_idnumber($idnumber) {
446         global $DB;
447         while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
448             $matches = array();
449             if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
450                 $newidnumber = $idnumber . '_2';
451             } else {
452                 $newidnumber = $matches[1] . ((int) $matches[2] + 1);
453             }
454             $idnumber = $newidnumber;
455         }
456         return $idnumber;
457     }
459     /**
460      * Helper to increment a shortname.
461      *
462      * This considers that the shortname passed has to be incremented.
463      *
464      * @param string $shortname shortname to increment.
465      * @return string new shortname.
466      */
467     public static function increment_shortname($shortname) {
468         global $DB;
469         do {
470             $matches = array();
471             if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
472                 $newshortname = $shortname . '_2';
473             } else {
474                 $newshortname = $matches[1] . ($matches[2]+1);
475             }
476             $shortname = $newshortname;
477         } while ($DB->record_exists('course', array('shortname' => $shortname)));
478         return $shortname;
479     }
481     /**
482      * Resolve a category based on the data passed.
483      *
484      * Key accepted are:
485      * - category, which is supposed to be a category ID.
486      * - category_idnumber
487      * - category_path, array of categories from parent to child.
488      *
489      * @param array $data to resolve the category from.
490      * @param array $errors will be populated with errors found.
491      * @return int category ID.
492      */
493     public static function resolve_category($data, &$errors = array()) {
494         $catid = null;
496         if (!empty($data['category'])) {
497             $category = core_course_category::get((int) $data['category'], IGNORE_MISSING);
498             if (!empty($category) && !empty($category->id)) {
499                 $catid = $category->id;
500             } else {
501                 $errors['couldnotresolvecatgorybyid'] =
502                     new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
503             }
504         }
506         if (empty($catid) && !empty($data['category_idnumber'])) {
507             $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
508             if (empty($catid)) {
509                 $errors['couldnotresolvecatgorybyidnumber'] =
510                     new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
511             }
512         }
513         if (empty($catid) && !empty($data['category_path'])) {
514             $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
515             if (empty($catid)) {
516                 $errors['couldnotresolvecatgorybypath'] =
517                     new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
518             }
519         }
521         return $catid;
522     }
524     /**
525      * Resolve a category by ID number.
526      *
527      * @param string $idnumber category ID number.
528      * @return int category ID.
529      */
530     public static function resolve_category_by_idnumber($idnumber) {
531         global $DB;
532         $cache = cache::make('tool_uploadcourse', 'helper');
533         $cachekey = 'cat_idn_' . $idnumber;
534         if (($id = $cache->get($cachekey)) === false) {
535             $params = array('idnumber' => $idnumber);
536             $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
538             // Little hack to be able to differenciate between the cache not set and a category not found.
539             if ($id === false) {
540                 $id = -1;
541             }
543             $cache->set($cachekey, $id);
544         }
546         // Little hack to be able to differenciate between the cache not set and a category not found.
547         if ($id == -1) {
548             $id = false;
549         }
551         return $id;
552     }
554     /**
555      * Resolve a category by path.
556      *
557      * @param array $path category names indexed from parent to children.
558      * @return int category ID.
559      */
560     public static function resolve_category_by_path(array $path) {
561         global $DB;
562         $cache = cache::make('tool_uploadcourse', 'helper');
563         $cachekey = 'cat_path_' . serialize($path);
564         if (($id = $cache->get($cachekey)) === false) {
565             $parent = 0;
566             $sql = 'name = :name AND parent = :parent';
567             while ($name = array_shift($path)) {
568                 $params = array('name' => $name, 'parent' => $parent);
569                 if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
570                     if (count($records) > 1) {
571                         // Too many records with the same name!
572                         $id = -1;
573                         break;
574                     }
575                     $record = reset($records);
576                     $id = $record->id;
577                     $parent = $record->id;
578                 } else {
579                     // Not found.
580                     $id = -1;
581                     break;
582                 }
583             }
584             $cache->set($cachekey, $id);
585         }
587         // We save -1 when the category has not been found to be able to know if the cache was set.
588         if ($id == -1) {
589             $id = false;
590         }
591         return $id;
592     }