See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401]
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 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'); 29 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 { 38 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 } 53 54 $course = (object) $data; 55 $fullname = isset($course->fullname) ? $course->fullname : ''; 56 $idnumber = isset($course->idnumber) ? $course->idnumber : ''; 57 58 $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber); 59 $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname); 60 61 if (!is_null($result)) { 62 $result = clean_param($result, PARAM_TEXT); 63 } 64 65 if (empty($result) && !is_numeric($result)) { 66 $result = null; 67 } 68 69 return $result; 70 } 71 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 } 91 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 } 103 104 if (!empty($block[2])) { 105 $repl = core_text::substr($repl, 0, $block[2]); 106 } 107 108 return $repl; 109 } 110 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 } 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 */ 138 public static function get_enrolment_data($data) { 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) { 163 if (!array_key_exists($method, $enrolmentplugins)) { 164 // Error! 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 * 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 } 188 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; 204 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 } 216 217 if (empty($cachekey)) { 218 return false; 219 } 220 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. 229 if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir(get_backup_temp_directory($backupid)))) { 230 231 // Use null instead of false because it would consider that the cache key has not been set. 232 $backupid = null; 233 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 } 257 258 if ($usecache) { 259 $cache->set($cachekey, $backupid); 260 } 261 } 262 263 if ($backupid === null) { 264 $backupid = false; 265 } 266 return $backupid; 267 } 268 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 } 288 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 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) { 320 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 } else if (preg_match('/^(.+)?_role$/', $field, $matches)) { 329 if (!isset($rolesids[$value])) { 330 $invalidroles[] = $value; 331 break; 332 } 333 } 334 335 } 336 337 if (!empty($invalidroles)) { 338 $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles)); 339 } 340 341 // Roles names. 342 return $rolenames; 343 } 344 345 /** 346 * Return array of all custom course fields indexed by their shortname 347 * 348 * @return \core_customfield\field_controller[] 349 */ 350 public static function get_custom_course_fields(): array { 351 $result = []; 352 353 $fields = \core_course\customfield\course_handler::create()->get_fields(); 354 foreach ($fields as $field) { 355 $result[$field->get('shortname')] = $field; 356 } 357 358 return $result; 359 } 360 361 /** 362 * Return array of custom field element names 363 * 364 * @return string[] 365 */ 366 public static function get_custom_course_field_names(): array { 367 $result = []; 368 369 $fields = self::get_custom_course_fields(); 370 foreach ($fields as $field) { 371 $controller = \core_customfield\data_controller::create(0, null, $field); 372 $result[] = $controller->get_form_element_name(); 373 } 374 375 return $result; 376 } 377 378 /** 379 * Return any elements from passed $data whose key matches one of the custom course fields defined for the site 380 * 381 * @param array $data 382 * @param array $defaults 383 * @param context $context 384 * @param array $errors Will be populated with any errors 385 * @return array 386 */ 387 public static function get_custom_course_field_data(array $data, array $defaults, context $context, 388 array &$errors = []): array { 389 390 $fields = self::get_custom_course_fields(); 391 $result = []; 392 393 $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context); 394 395 foreach ($data as $name => $originalvalue) { 396 if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches) 397 && isset($fields[$matches['name']])) { 398 399 $fieldname = $matches['name']; 400 $field = $fields[$fieldname]; 401 402 // Skip field if it's locked and user doesn't have capability to change locked fields. 403 if ($field->get_configdata_property('locked') && !$canchangelockedfields) { 404 continue; 405 } 406 407 // Create field data controller. 408 $controller = \core_customfield\data_controller::create(0, null, $field); 409 $controller->set('id', 1); 410 411 $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value(); 412 $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue)); 413 414 // If we initially had a value, but now don't, then reset it to the default. 415 if (!empty($originalvalue) && empty($value)) { 416 $value = $defaultvalue; 417 } 418 419 // Validate data with controller. 420 $fieldformdata = [$controller->get_form_element_name() => $value]; 421 $validationerrors = $controller->instance_form_validation($fieldformdata, []); 422 if (count($validationerrors) > 0) { 423 $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse', 424 $field->get_formatted_name()); 425 426 continue; 427 } 428 429 $controller->set($controller->datafield(), $value); 430 431 // Pass an empty object to the data controller, which will transform it to a correct name/value pair. 432 $instance = new stdClass(); 433 $controller->instance_form_before_set_data($instance); 434 435 $result = array_merge($result, (array) $instance); 436 } 437 } 438 439 return $result; 440 } 441 442 /** 443 * Helper to increment an ID number. 444 * 445 * This first checks if the ID number is in use. 446 * 447 * @param string $idnumber ID number to increment. 448 * @return string new ID number. 449 */ 450 public static function increment_idnumber($idnumber) { 451 global $DB; 452 while ($DB->record_exists('course', array('idnumber' => $idnumber))) { 453 $matches = array(); 454 if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) { 455 $newidnumber = $idnumber . '_2'; 456 } else { 457 $newidnumber = $matches[1] . ((int) $matches[2] + 1); 458 } 459 $idnumber = $newidnumber; 460 } 461 return $idnumber; 462 } 463 464 /** 465 * Helper to increment a shortname. 466 * 467 * This considers that the shortname passed has to be incremented. 468 * 469 * @param string $shortname shortname to increment. 470 * @return string new shortname. 471 */ 472 public static function increment_shortname($shortname) { 473 global $DB; 474 do { 475 $matches = array(); 476 if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) { 477 $newshortname = $shortname . '_2'; 478 } else { 479 $newshortname = $matches[1] . ($matches[2]+1); 480 } 481 $shortname = $newshortname; 482 } while ($DB->record_exists('course', array('shortname' => $shortname))); 483 return $shortname; 484 } 485 486 /** 487 * Resolve a category based on the data passed. 488 * 489 * Key accepted are: 490 * - category, which is supposed to be a category ID. 491 * - category_idnumber 492 * - category_path, array of categories from parent to child. 493 * 494 * @param array $data to resolve the category from. 495 * @param array $errors will be populated with errors found. 496 * @return int category ID. 497 */ 498 public static function resolve_category($data, &$errors = array()) { 499 $catid = null; 500 501 if (!empty($data['category'])) { 502 $category = core_course_category::get((int) $data['category'], IGNORE_MISSING); 503 if (!empty($category) && !empty($category->id)) { 504 $catid = $category->id; 505 } else { 506 $errors['couldnotresolvecatgorybyid'] = 507 new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse'); 508 } 509 } 510 511 if (empty($catid) && !empty($data['category_idnumber'])) { 512 $catid = self::resolve_category_by_idnumber($data['category_idnumber']); 513 if (empty($catid)) { 514 $errors['couldnotresolvecatgorybyidnumber'] = 515 new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse'); 516 } 517 } 518 if (empty($catid) && !empty($data['category_path'])) { 519 $catid = self::resolve_category_by_path(explode(' / ', $data['category_path'])); 520 if (empty($catid)) { 521 $errors['couldnotresolvecatgorybypath'] = 522 new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse'); 523 } 524 } 525 526 return $catid; 527 } 528 529 /** 530 * Resolve a category by ID number. 531 * 532 * @param string $idnumber category ID number. 533 * @return int category ID. 534 */ 535 public static function resolve_category_by_idnumber($idnumber) { 536 global $DB; 537 $cache = cache::make('tool_uploadcourse', 'helper'); 538 $cachekey = 'cat_idn_' . $idnumber; 539 if (($id = $cache->get($cachekey)) === false) { 540 $params = array('idnumber' => $idnumber); 541 $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING); 542 543 // Little hack to be able to differenciate between the cache not set and a category not found. 544 if ($id === false) { 545 $id = -1; 546 } 547 548 $cache->set($cachekey, $id); 549 } 550 551 // Little hack to be able to differenciate between the cache not set and a category not found. 552 if ($id == -1) { 553 $id = false; 554 } 555 556 return $id; 557 } 558 559 /** 560 * Resolve a category by path. 561 * 562 * @param array $path category names indexed from parent to children. 563 * @return int category ID. 564 */ 565 public static function resolve_category_by_path(array $path) { 566 global $DB; 567 $cache = cache::make('tool_uploadcourse', 'helper'); 568 $cachekey = 'cat_path_' . serialize($path); 569 if (($id = $cache->get($cachekey)) === false) { 570 $parent = 0; 571 $sql = 'name = :name AND parent = :parent'; 572 while ($name = array_shift($path)) { 573 $params = array('name' => $name, 'parent' => $parent); 574 if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) { 575 if (count($records) > 1) { 576 // Too many records with the same name! 577 $id = -1; 578 break; 579 } 580 $record = reset($records); 581 $id = $record->id; 582 $parent = $record->id; 583 } else { 584 // Not found. 585 $id = -1; 586 break; 587 } 588 } 589 $cache->set($cachekey, $id); 590 } 591 592 // We save -1 when the category has not been found to be able to know if the cache was set. 593 if ($id == -1) { 594 $id = false; 595 } 596 return $id; 597 } 598 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body