on-demand release 4.0dev+
[moodle.git] / admin / tool / uploadcourse / classes / helper.php
CommitLineData
9a7cd639
FM
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/>.
16
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 */
24
25defined('MOODLE_INTERNAL') || die();
8c313d1a 26require_once($CFG->dirroot . '/cache/lib.php');
e0164eb3
FM
27require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
28require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
9a7cd639
FM
29
30/**
31 * Class containing a set of helpers.
3f57c87e
FM
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
9a7cd639
FM
36 */
37class tool_uploadcourse_helper {
38
9a7cd639
FM
39 /**
40 * Generate a shortname based on a template.
41 *
42 * @param array|object $data course data.
43 * @param string $templateshortname template of shortname.
f802b04f 44 * @return null|string shortname based on the template, or null when an error occured.
9a7cd639
FM
45 */
46 public static function generate_shortname($data, $templateshortname) {
f802b04f 47 if (empty($templateshortname) && !is_numeric($templateshortname)) {
9a7cd639
FM
48 return null;
49 }
50 if (strpos($templateshortname, '%') === false) {
51 return $templateshortname;
52 }
53
54 $course = (object) $data;
9a7cd639
FM
55 $fullname = isset($course->fullname) ? $course->fullname : '';
56 $idnumber = isset($course->idnumber) ? $course->idnumber : '';
57
f802b04f 58 $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
9a7cd639
FM
59 $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
60
61 if (!is_null($result)) {
62 $result = clean_param($result, PARAM_TEXT);
63 }
64
f802b04f
FM
65 if (empty($result) && !is_numeric($result)) {
66 $result = null;
67 }
68
9a7cd639
FM
69 return $result;
70 }
71
72 /**
73 * Callback used when generating a shortname based on a template.
74 *
9a7cd639
FM
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 */
f802b04f 80 public static function generate_shortname_callback($fullname, $idnumber, $block) {
9a7cd639
FM
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 }
91
92 switch ($block[1]) {
93 case '+':
2f1e464a 94 $repl = core_text::strtoupper($repl);
9a7cd639
FM
95 break;
96 case '-':
2f1e464a 97 $repl = core_text::strtolower($repl);
9a7cd639
FM
98 break;
99 case '~':
2f1e464a 100 $repl = core_text::strtotitle($repl);
9a7cd639
FM
101 break;
102 }
103
104 if (!empty($block[2])) {
2f1e464a 105 $repl = core_text::substr($repl, 0, $block[2]);
9a7cd639
FM
106 }
107
108 return $repl;
109 }
110
111 /**
112 * Return the available course formats.
113 *
9a7cd639
FM
114 * @return array
115 */
116 public static function get_course_formats() {
41525e55 117 return array_keys(core_component::get_plugin_list('format'));
9a7cd639
FM
118 }
119
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 */
853b6cff 138 public static function get_enrolment_data($data) {
9a7cd639
FM
139 $enrolmethods = array();
140 $enroloptions = array();
141 foreach ($data as $field => $value) {
142
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 }
157
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) {
e6f5a4b2 163 if (!array_key_exists($method, $enrolmentplugins)) {
853b6cff 164 // Error!
9a7cd639
FM
165 continue;
166 }
167 $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
168 }
169 }
170 return $enrolmentdata;
171 }
172
173 /**
174 * Return the enrolment plugins.
175 *
176 * The result is cached for faster execution.
177 *
b9a09480 178 * @return enrol_plugin[]
9a7cd639
FM
179 */
180 public static function get_enrolment_plugins() {
8c313d1a
FM
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);
9a7cd639 185 }
8c313d1a 186 return $enrol;
9a7cd639
FM
187 }
188
189 /**
190 * Get the restore content tempdir.
191 *
192 * The tempdir is the sub directory in which the backup has been extracted.
d90b2eb9
FM
193 *
194 * This caches the result for better performance, but $CFG->keeptempdirectoriesonbackup
195 * needs to be enabled, otherwise the cache is ignored.
9a7cd639
FM
196 *
197 * @param string $backupfile path to a backup file.
198 * @param string $shortname shortname of a course.
1d1898ac 199 * @param array $errors will be populated with errors found.
9a7cd639
FM
200 * @return string|false false when the backup couldn't retrieved.
201 */
1d1898ac 202 public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
9a7cd639
FM
203 global $CFG, $DB, $USER;
204
205 $cachekey = null;
206 if (!empty($backupfile)) {
207 $backupfile = realpath($backupfile);
d90b2eb9
FM
208 if (empty($backupfile) || !is_readable($backupfile)) {
209 $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
210 return false;
211 }
8c313d1a 212 $cachekey = 'backup_path:' . $backupfile;
9a7cd639 213 } else if (!empty($shortname) || is_numeric($shortname)) {
8c313d1a 214 $cachekey = 'backup_sn:' . $shortname;
9a7cd639
FM
215 }
216
217 if (empty($cachekey)) {
218 return false;
219 }
220
d90b2eb9
FM
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 }
227
228 // If we don't use the cache, or if we do and not set, or the directory doesn't exist any more.
ef844148 229 if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir(get_backup_temp_directory($backupid)))) {
d90b2eb9
FM
230
231 // Use null instead of false because it would consider that the cache key has not been set.
232 $backupid = null;
233
1d1898ac 234 if (!empty($backupfile)) {
d90b2eb9
FM
235 // Extracting the backup file.
236 $packer = get_file_packer('application/vnd.moodle.backup');
237 $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
ef844148 238 $path = make_backup_temp_directory($backupid, false);
d90b2eb9
FM
239 $result = $packer->extract_to_pathname($backupfile, $path);
240 if (!$result) {
241 $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
1d1898ac 242 }
9a7cd639
FM
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();
1d1898ac
FM
252 } else {
253 $errors['coursetorestorefromdoesnotexist'] =
254 new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
9a7cd639
FM
255 }
256 }
8c313d1a 257
d90b2eb9
FM
258 if ($usecache) {
259 $cache->set($cachekey, $backupid);
260 }
9a7cd639
FM
261 }
262
d90b2eb9
FM
263 if ($backupid === null) {
264 $backupid = false;
265 }
8c313d1a 266 return $backupid;
9a7cd639
FM
267 }
268
269 /**
270 * Return the role IDs.
271 *
272 * The result is cached for faster execution.
273 *
9a7cd639
FM
274 * @return array
275 */
8c313d1a
FM
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;
9a7cd639 283 }
8c313d1a 284 $cache->set('roles', $roles);
9a7cd639 285 }
8c313d1a 286 return $roles;
9a7cd639
FM
287 }
288
d4beb211
LB
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 }
307
9a7cd639
FM
308 /**
309 * Get the role renaming data from the passed data.
310 *
311 * @param array $data data to extract the names from.
1d1898ac 312 * @param array $errors will be populated with errors found.
9a7cd639
FM
313 * @return array where the key is the role_<id>, the value is the new name.
314 */
1d1898ac 315 public static function get_role_names($data, &$errors = array()) {
9a7cd639
FM
316 $rolenames = array();
317 $rolesids = self::get_role_ids();
1d1898ac 318 $invalidroles = array();
9a7cd639
FM
319 foreach ($data as $field => $value) {
320
321 $matches = array();
322 if (preg_match('/^role_(.+)?$/', $field, $matches)) {
323 if (!isset($rolesids[$matches[1]])) {
1d1898ac 324 $invalidroles[] = $matches[1];
9a7cd639
FM
325 continue;
326 }
327 $rolenames['role_' . $rolesids[$matches[1]]] = $value;
328 }
329
330 }
331
1d1898ac
FM
332 if (!empty($invalidroles)) {
333 $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
334 }
335
9a7cd639
FM
336 // Roles names.
337 return $rolenames;
338 }
339
d62fc08e
PH
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 = [];
347
348 $fields = \core_course\customfield\course_handler::create()->get_fields();
349 foreach ($fields as $field) {
350 $result[$field->get('shortname')] = $field;
351 }
352
353 return $result;
354 }
355
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 = [];
363
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 }
369
370 return $result;
371 }
372
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 {
384
385 $fields = self::get_custom_course_fields();
386 $result = [];
387
388 $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
389
390 foreach ($data as $name => $originalvalue) {
391 if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
392 && isset($fields[$matches['name']])) {
393
394 $fieldname = $matches['name'];
395 $field = $fields[$fieldname];
396
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 }
401
402 // Create field data controller.
403 $controller = \core_customfield\data_controller::create(0, null, $field);
404 $controller->set('id', 1);
405
406 $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
407 $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
408
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 }
413
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());
420
421 continue;
422 }
423
424 $controller->set($controller->datafield(), $value);
425
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);
429
430 $result = array_merge($result, (array) $instance);
431 }
432 }
433
434 return $result;
435 }
436
9a7cd639
FM
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 }
458
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 }
480
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.
1d1898ac 490 * @param array $errors will be populated with errors found.
9a7cd639
FM
491 * @return int category ID.
492 */
1d1898ac 493 public static function resolve_category($data, &$errors = array()) {
9a7cd639
FM
494 $catid = null;
495
496 if (!empty($data['category'])) {
442f12f8 497 $category = core_course_category::get((int) $data['category'], IGNORE_MISSING);
8383903f 498 if (!empty($category) && !empty($category->id)) {
9a7cd639 499 $catid = $category->id;
1d1898ac
FM
500 } else {
501 $errors['couldnotresolvecatgorybyid'] =
502 new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
9a7cd639
FM
503 }
504 }
505
506 if (empty($catid) && !empty($data['category_idnumber'])) {
507 $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
8383903f
FM
508 if (empty($catid)) {
509 $errors['couldnotresolvecatgorybyidnumber'] =
510 new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
511 }
9a7cd639 512 }
9a7cd639
FM
513 if (empty($catid) && !empty($data['category_path'])) {
514 $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
8383903f
FM
515 if (empty($catid)) {
516 $errors['couldnotresolvecatgorybypath'] =
517 new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
518 }
9a7cd639
FM
519 }
520
521 return $catid;
522 }
523
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;
8c313d1a
FM
532 $cache = cache::make('tool_uploadcourse', 'helper');
533 $cachekey = 'cat_idn_' . $idnumber;
534 if (($id = $cache->get($cachekey)) === false) {
9a7cd639
FM
535 $params = array('idnumber' => $idnumber);
536 $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
8c313d1a
FM
537
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 }
542
543 $cache->set($cachekey, $id);
544 }
545
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;
9a7cd639 549 }
8c313d1a
FM
550
551 return $id;
9a7cd639
FM
552 }
553
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;
8c313d1a
FM
562 $cache = cache::make('tool_uploadcourse', 'helper');
563 $cachekey = 'cat_path_' . serialize($path);
564 if (($id = $cache->get($cachekey)) === false) {
9a7cd639
FM
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!
8c313d1a 572 $id = -1;
9a7cd639
FM
573 break;
574 }
575 $record = reset($records);
576 $id = $record->id;
577 $parent = $record->id;
578 } else {
579 // Not found.
8c313d1a 580 $id = -1;
9a7cd639
FM
581 break;
582 }
583 }
8c313d1a
FM
584 $cache->set($cachekey, $id);
585 }
586
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;
9a7cd639 590 }
8c313d1a 591 return $id;
9a7cd639 592 }
d62fc08e 593}