Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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 } 329 330 } 331 332 if (!empty($invalidroles)) { 333 $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles)); 334 } 335 336 // Roles names. 337 return $rolenames; 338 } 339 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 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. 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; 495 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 } 505 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 } 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; 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); 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; 549 } 550 551 return $id; 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; 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 } 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; 590 } 591 return $id; 592 } 593 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body