Differences Between: [Versions 310 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
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 course 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 . '/backup/util/includes/restore_includes.php'); 27 require_once($CFG->dirroot . '/course/lib.php'); 28 29 /** 30 * Course class. 31 * 32 * @package tool_uploadcourse 33 * @copyright 2013 Frédéric Massart 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class tool_uploadcourse_course { 37 38 /** Outcome of the process: creating the course */ 39 const DO_CREATE = 1; 40 41 /** Outcome of the process: updating the course */ 42 const DO_UPDATE = 2; 43 44 /** Outcome of the process: deleting the course */ 45 const DO_DELETE = 3; 46 47 /** @var array assignable roles. */ 48 protected $assignableroles = []; 49 50 /** @var array Roles context levels. */ 51 protected $contextlevels = []; 52 53 /** @var array final import data. */ 54 protected $data = array(); 55 56 /** @var array default values. */ 57 protected $defaults = array(); 58 59 /** @var array enrolment data. */ 60 protected $enrolmentdata; 61 62 /** @var array errors. */ 63 protected $errors = array(); 64 65 /** @var int the ID of the course that had been processed. */ 66 protected $id; 67 68 /** @var array containing options passed from the processor. */ 69 protected $importoptions = array(); 70 71 /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */ 72 protected $mode; 73 74 /** @var array course import options. */ 75 protected $options = array(); 76 77 /** @var int constant value of self::DO_*, what to do with that course */ 78 protected $do; 79 80 /** @var bool set to true once we have prepared the course */ 81 protected $prepared = false; 82 83 /** @var bool set to true once we have started the process of the course */ 84 protected $processstarted = false; 85 86 /** @var array course import data. */ 87 protected $rawdata = array(); 88 89 /** @var array restore directory. */ 90 protected $restoredata; 91 92 /** @var string course shortname. */ 93 protected $shortname; 94 95 /** @var array errors. */ 96 protected $statuses = array(); 97 98 /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */ 99 protected $updatemode; 100 101 /** @var array fields allowed as course data. */ 102 static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', 'enddate', 103 'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes', 104 'groupmode', 'groupmodeforce', 'enablecompletion', 'downloadcontent'); 105 106 /** @var array fields required on course creation. */ 107 static protected $mandatoryfields = array('fullname', 'category'); 108 109 /** @var array fields which are considered as options. */ 110 static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null, 111 'templatecourse' => null, 'reset' => false); 112 113 /** @var array options determining what can or cannot be done at an import level. */ 114 static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false, 115 'reset' => false, 'restoredir' => null, 'shortnametemplate' => null); 116 117 /** 118 * Constructor 119 * 120 * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_* 121 * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_* 122 * @param array $rawdata raw course data. 123 * @param array $defaults default course data. 124 * @param array $importoptions import options. 125 */ 126 public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) { 127 128 if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW && 129 $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL && 130 $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE && 131 $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) { 132 throw new coding_exception('Incorrect mode.'); 133 } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING && 134 $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY && 135 $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS && 136 $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) { 137 throw new coding_exception('Incorrect update mode.'); 138 } 139 140 $this->mode = $mode; 141 $this->updatemode = $updatemode; 142 143 if (isset($rawdata['shortname'])) { 144 $this->shortname = $rawdata['shortname']; 145 } 146 $this->rawdata = $rawdata; 147 $this->defaults = $defaults; 148 149 // Extract course options. 150 foreach (self::$optionfields as $option => $default) { 151 $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default; 152 } 153 154 // Import options. 155 foreach (self::$importoptionsdefaults as $option => $default) { 156 $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default; 157 } 158 } 159 160 /** 161 * Does the mode allow for course creation? 162 * 163 * @return bool 164 */ 165 public function can_create() { 166 return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, 167 tool_uploadcourse_processor::MODE_CREATE_NEW, 168 tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE) 169 ); 170 } 171 172 /** 173 * Does the mode allow for course deletion? 174 * 175 * @return bool 176 */ 177 public function can_delete() { 178 return $this->importoptions['candelete']; 179 } 180 181 /** 182 * Does the mode only allow for course creation? 183 * 184 * @return bool 185 */ 186 public function can_only_create() { 187 return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, 188 tool_uploadcourse_processor::MODE_CREATE_NEW)); 189 } 190 191 /** 192 * Does the mode allow for course rename? 193 * 194 * @return bool 195 */ 196 public function can_rename() { 197 return $this->importoptions['canrename']; 198 } 199 200 /** 201 * Does the mode allow for course reset? 202 * 203 * @return bool 204 */ 205 public function can_reset() { 206 return $this->importoptions['canreset']; 207 } 208 209 /** 210 * Does the mode allow for course update? 211 * 212 * @return bool 213 */ 214 public function can_update() { 215 return in_array($this->mode, 216 array( 217 tool_uploadcourse_processor::MODE_UPDATE_ONLY, 218 tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE) 219 ) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING; 220 } 221 222 /** 223 * Can we use default values? 224 * 225 * @return bool 226 */ 227 public function can_use_defaults() { 228 return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS, 229 tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS)); 230 } 231 232 /** 233 * Delete the current course. 234 * 235 * @return bool 236 */ 237 protected function delete() { 238 global $DB; 239 $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname', 240 array('shortname' => $this->shortname), MUST_EXIST); 241 return delete_course($this->id, false); 242 } 243 244 /** 245 * Log an error 246 * 247 * @param string $code error code. 248 * @param lang_string $message error message. 249 * @return void 250 */ 251 protected function error($code, lang_string $message) { 252 if (array_key_exists($code, $this->errors)) { 253 throw new coding_exception('Error code already defined'); 254 } 255 $this->errors[$code] = $message; 256 } 257 258 /** 259 * Return whether the course exists or not. 260 * 261 * @param string $shortname the shortname to use to check if the course exists. Falls back on $this->shortname if empty. 262 * @return bool 263 */ 264 protected function exists($shortname = null) { 265 global $DB; 266 if (is_null($shortname)) { 267 $shortname = $this->shortname; 268 } 269 if (!empty($shortname) || is_numeric($shortname)) { 270 return $DB->record_exists('course', array('shortname' => $shortname)); 271 } 272 return false; 273 } 274 275 /** 276 * Return the data that will be used upon saving. 277 * 278 * @return null|array 279 */ 280 public function get_data() { 281 return $this->data; 282 } 283 284 /** 285 * Return the errors found during preparation. 286 * 287 * @return array 288 */ 289 public function get_errors() { 290 return $this->errors; 291 } 292 293 /** 294 * Return array of valid fields for default values 295 * 296 * @return array 297 */ 298 protected function get_valid_fields() { 299 return array_merge(self::$validfields, \tool_uploadcourse_helper::get_custom_course_field_names()); 300 } 301 302 /** 303 * Assemble the course data based on defaults. 304 * 305 * This returns the final data to be passed to create_course(). 306 * 307 * @param array $data current data. 308 * @return array 309 */ 310 protected function get_final_create_data($data) { 311 foreach ($this->get_valid_fields() as $field) { 312 if (!isset($data[$field]) && isset($this->defaults[$field])) { 313 $data[$field] = $this->defaults[$field]; 314 } 315 } 316 $data['shortname'] = $this->shortname; 317 return $data; 318 } 319 320 /** 321 * Assemble the course data based on defaults. 322 * 323 * This returns the final data to be passed to update_course(). 324 * 325 * @param array $data current data. 326 * @param bool $usedefaults are defaults allowed? 327 * @param bool $missingonly ignore fields which are already set. 328 * @return array 329 */ 330 protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) { 331 global $DB; 332 $newdata = array(); 333 $existingdata = $DB->get_record('course', array('shortname' => $this->shortname)); 334 foreach ($this->get_valid_fields() as $field) { 335 if ($missingonly) { 336 if (isset($existingdata->$field) and $existingdata->$field !== '') { 337 continue; 338 } 339 } 340 if (isset($data[$field])) { 341 $newdata[$field] = $data[$field]; 342 } else if ($usedefaults && isset($this->defaults[$field])) { 343 $newdata[$field] = $this->defaults[$field]; 344 } 345 } 346 $newdata['id'] = $existingdata->id; 347 return $newdata; 348 } 349 350 /** 351 * Return the ID of the processed course. 352 * 353 * @return int|null 354 */ 355 public function get_id() { 356 if (!$this->processstarted) { 357 throw new coding_exception('The course has not been processed yet!'); 358 } 359 return $this->id; 360 } 361 362 /** 363 * Get the directory of the object to restore. 364 * 365 * @return string|false|null subdirectory in $CFG->backuptempdir/..., false when an error occured 366 * and null when there is simply nothing. 367 */ 368 protected function get_restore_content_dir() { 369 $backupfile = null; 370 $shortname = null; 371 372 if (!empty($this->options['backupfile'])) { 373 $backupfile = $this->options['backupfile']; 374 } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) { 375 $shortname = $this->options['templatecourse']; 376 } 377 378 $errors = array(); 379 $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname, $errors); 380 if (!empty($errors)) { 381 foreach ($errors as $key => $message) { 382 $this->error($key, $message); 383 } 384 return false; 385 } else if ($dir === false) { 386 // We want to return null when nothing was wrong, but nothing was found. 387 $dir = null; 388 } 389 390 if (empty($dir) && !empty($this->importoptions['restoredir'])) { 391 $dir = $this->importoptions['restoredir']; 392 } 393 394 return $dir; 395 } 396 397 /** 398 * Return the errors found during preparation. 399 * 400 * @return array 401 */ 402 public function get_statuses() { 403 return $this->statuses; 404 } 405 406 /** 407 * Return whether there were errors with this course. 408 * 409 * @return boolean 410 */ 411 public function has_errors() { 412 return !empty($this->errors); 413 } 414 415 /** 416 * Validates and prepares the data. 417 * 418 * @return bool false is any error occured. 419 */ 420 public function prepare() { 421 global $DB, $SITE, $CFG; 422 423 $this->prepared = true; 424 425 // Validate the shortname. 426 if (!empty($this->shortname) || is_numeric($this->shortname)) { 427 if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) { 428 $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); 429 return false; 430 } 431 432 // Ensure we don't overflow the maximum length of the shortname field. 433 if (core_text::strlen($this->shortname) > 255) { 434 $this->error('invalidshortnametoolong', new lang_string('invalidshortnametoolong', 'tool_uploadcourse', 255)); 435 return false; 436 } 437 } 438 439 $exists = $this->exists(); 440 441 // Do we want to delete the course? 442 if ($this->options['delete']) { 443 if (!$exists) { 444 $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse')); 445 return false; 446 } else if (!$this->can_delete()) { 447 $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse')); 448 return false; 449 } 450 451 $this->do = self::DO_DELETE; 452 return true; 453 } 454 455 // Can we create/update the course under those conditions? 456 if ($exists) { 457 if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { 458 $this->error('courseexistsanduploadnotallowed', 459 new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); 460 return false; 461 } else if ($this->can_update()) { 462 // We can never allow for any front page changes! 463 if ($this->shortname == $SITE->shortname) { 464 $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse')); 465 return false; 466 } 467 } 468 } else { 469 if (!$this->can_create()) { 470 $this->error('coursedoesnotexistandcreatenotallowed', 471 new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse')); 472 return false; 473 } 474 } 475 476 // Basic data. 477 $coursedata = array(); 478 foreach ($this->rawdata as $field => $value) { 479 if (!in_array($field, self::$validfields)) { 480 continue; 481 } else if ($field == 'shortname') { 482 // Let's leave it apart from now, use $this->shortname only. 483 continue; 484 } 485 $coursedata[$field] = $value; 486 } 487 488 $mode = $this->mode; 489 $updatemode = $this->updatemode; 490 $usedefaults = $this->can_use_defaults(); 491 492 // Resolve the category, and fail if not found. 493 $errors = array(); 494 $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors); 495 if (empty($errors)) { 496 $coursedata['category'] = $catid; 497 } else { 498 foreach ($errors as $key => $message) { 499 $this->error($key, $message); 500 } 501 return false; 502 } 503 504 // Ensure we don't overflow the maximum length of the fullname field. 505 if (!empty($coursedata['fullname']) && core_text::strlen($coursedata['fullname']) > 254) { 506 $this->error('invalidfullnametoolong', new lang_string('invalidfullnametoolong', 'tool_uploadcourse', 254)); 507 return false; 508 } 509 510 // If the course does not exist, or will be forced created. 511 if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { 512 513 // Mandatory fields upon creation. 514 $errors = array(); 515 foreach (self::$mandatoryfields as $field) { 516 if ((!isset($coursedata[$field]) || $coursedata[$field] === '') && 517 (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) { 518 $errors[] = $field; 519 } 520 } 521 if (!empty($errors)) { 522 $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse', 523 implode(', ', $errors))); 524 return false; 525 } 526 } 527 528 // Should the course be renamed? 529 if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) { 530 if (!$this->can_update()) { 531 $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse')); 532 return false; 533 } else if (!$exists) { 534 $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse')); 535 return false; 536 } else if (!$this->can_rename()) { 537 $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse')); 538 return false; 539 } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) { 540 $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); 541 return false; 542 } else if ($this->exists($this->options['rename'])) { 543 $this->error('cannotrenameshortnamealreadyinuse', 544 new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse')); 545 return false; 546 } else if (isset($coursedata['idnumber']) && 547 $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn', 548 array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) { 549 $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse')); 550 return false; 551 } 552 $coursedata['shortname'] = $this->options['rename']; 553 $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse', 554 array('from' => $this->shortname, 'to' => $coursedata['shortname']))); 555 } 556 557 // Should we generate a shortname? 558 if (empty($this->shortname) && !is_numeric($this->shortname)) { 559 if (empty($this->importoptions['shortnametemplate'])) { 560 $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse')); 561 return false; 562 } else if (!$this->can_only_create()) { 563 $this->error('cannotgenerateshortnameupdatemode', 564 new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse')); 565 return false; 566 } else { 567 $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata, 568 $this->importoptions['shortnametemplate']); 569 if (is_null($newshortname)) { 570 $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse')); 571 return false; 572 } else if ($this->exists($newshortname)) { 573 if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { 574 $this->error('generatedshortnamealreadyinuse', 575 new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse')); 576 return false; 577 } 578 $exists = true; 579 } 580 $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse', 581 $newshortname)); 582 $this->shortname = $newshortname; 583 } 584 } 585 586 // If exists, but we only want to create courses, increment the shortname. 587 if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { 588 $original = $this->shortname; 589 $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname); 590 $exists = false; 591 if ($this->shortname != $original) { 592 $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse', 593 array('from' => $original, 'to' => $this->shortname))); 594 if (isset($coursedata['idnumber'])) { 595 $originalidn = $coursedata['idnumber']; 596 $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']); 597 if ($originalidn != $coursedata['idnumber']) { 598 $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse', 599 array('from' => $originalidn, 'to' => $coursedata['idnumber']))); 600 } 601 } 602 } 603 } 604 605 // If the course does not exist, ensure that the ID number is not taken. 606 if (!$exists && isset($coursedata['idnumber'])) { 607 if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) { 608 $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse')); 609 return false; 610 } 611 } 612 613 // Course start date. 614 if (!empty($coursedata['startdate'])) { 615 $coursedata['startdate'] = strtotime($coursedata['startdate']); 616 } 617 618 // Course end date. 619 if (!empty($coursedata['enddate'])) { 620 $coursedata['enddate'] = strtotime($coursedata['enddate']); 621 } 622 623 // If lang is specified, check the user is allowed to set that field. 624 if (!empty($coursedata['lang'])) { 625 if ($exists) { 626 $courseid = $DB->get_field('course', 'id', ['shortname' => $this->shortname]); 627 if (!has_capability('moodle/course:setforcedlanguage', context_course::instance($courseid))) { 628 $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse')); 629 return false; 630 } 631 } else { 632 $catcontext = context_coursecat::instance($coursedata['category']); 633 if (!guess_if_creator_will_have_course_capability('moodle/course:setforcedlanguage', $catcontext)) { 634 $this->error('cannotforcelang', new lang_string('cannotforcelang', 'tool_uploadcourse')); 635 return false; 636 } 637 } 638 } 639 640 // Ultimate check mode vs. existence. 641 switch ($mode) { 642 case tool_uploadcourse_processor::MODE_CREATE_NEW: 643 case tool_uploadcourse_processor::MODE_CREATE_ALL: 644 if ($exists) { 645 $this->error('courseexistsanduploadnotallowed', 646 new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); 647 return false; 648 } 649 break; 650 case tool_uploadcourse_processor::MODE_UPDATE_ONLY: 651 if (!$exists) { 652 $this->error('coursedoesnotexistandcreatenotallowed', 653 new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse')); 654 return false; 655 } 656 // No break! 657 case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE: 658 if ($exists) { 659 if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) { 660 $this->error('updatemodedoessettonothing', 661 new lang_string('updatemodedoessettonothing', 'tool_uploadcourse')); 662 return false; 663 } 664 } 665 break; 666 default: 667 // O_o Huh?! This should really never happen here! 668 $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse')); 669 return false; 670 } 671 672 // Get final data. 673 if ($exists) { 674 $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS); 675 $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly); 676 677 // Make sure we are not trying to mess with the front page, though we should never get here! 678 if ($coursedata['id'] == $SITE->id) { 679 $this->error('cannotupdatefrontpage', new lang_string('cannotupdatefrontpage', 'tool_uploadcourse')); 680 return false; 681 } 682 683 $this->do = self::DO_UPDATE; 684 } else { 685 $coursedata = $this->get_final_create_data($coursedata); 686 $this->do = self::DO_CREATE; 687 } 688 689 // Validate course start and end dates. 690 if ($exists) { 691 // We also check existing start and end dates if we are updating an existing course. 692 $existingdata = $DB->get_record('course', array('shortname' => $this->shortname)); 693 if (empty($coursedata['startdate'])) { 694 $coursedata['startdate'] = $existingdata->startdate; 695 } 696 if (empty($coursedata['enddate'])) { 697 $coursedata['enddate'] = $existingdata->enddate; 698 } 699 } 700 if ($errorcode = course_validate_dates($coursedata)) { 701 $this->error($errorcode, new lang_string($errorcode, 'error')); 702 return false; 703 } 704 705 // Add role renaming. 706 $errors = array(); 707 $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors); 708 if (!empty($errors)) { 709 foreach ($errors as $key => $message) { 710 $this->error($key, $message); 711 } 712 return false; 713 } 714 foreach ($rolenames as $rolekey => $rolename) { 715 $coursedata[$rolekey] = $rolename; 716 } 717 718 // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context. 719 if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) { 720 $context = context_course::instance($coursedata['id']); 721 } else { 722 // The category ID is taken from the defaults if it exists, otherwise from course data. 723 $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']); 724 } 725 $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context, 726 $errors); 727 if (!empty($errors)) { 728 foreach ($errors as $key => $message) { 729 $this->error($key, $message); 730 } 731 732 return false; 733 } 734 735 foreach ($customfielddata as $name => $value) { 736 $coursedata[$name] = $value; 737 } 738 739 // Some validation. 740 if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) { 741 $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse')); 742 return false; 743 } 744 745 // Add data for course format options. 746 if (isset($coursedata['format']) || $exists) { 747 if (isset($coursedata['format'])) { 748 $courseformat = course_get_format((object)['format' => $coursedata['format']]); 749 } else { 750 $courseformat = course_get_format($existingdata); 751 } 752 $coursedata += $courseformat->validate_course_format_options($this->rawdata); 753 } 754 755 // Special case, 'numsections' is not a course format option any more but still should apply from the template course, 756 // if any, and otherwise from defaults. 757 if (!$exists || !array_key_exists('numsections', $coursedata)) { 758 if (isset($this->rawdata['numsections']) && is_numeric($this->rawdata['numsections'])) { 759 $coursedata['numsections'] = (int)$this->rawdata['numsections']; 760 } else if (isset($this->options['templatecourse'])) { 761 $numsections = tool_uploadcourse_helper::get_coursesection_count($this->options['templatecourse']); 762 if ($numsections != 0) { 763 $coursedata['numsections'] = $numsections; 764 } else { 765 $coursedata['numsections'] = get_config('moodlecourse', 'numsections'); 766 } 767 } else { 768 $coursedata['numsections'] = get_config('moodlecourse', 'numsections'); 769 } 770 } 771 772 // Visibility can only be 0 or 1. 773 if (!empty($coursedata['visible']) AND !($coursedata['visible'] == 0 OR $coursedata['visible'] == 1)) { 774 $this->error('invalidvisibilitymode', new lang_string('invalidvisibilitymode', 'tool_uploadcourse')); 775 return false; 776 } 777 778 // Ensure that user is allowed to configure course content download and the field contains a valid value. 779 if (isset($coursedata['downloadcontent'])) { 780 if (!$CFG->downloadcoursecontentallowed || 781 !has_capability('moodle/course:configuredownloadcontent', $context)) { 782 783 $this->error('downloadcontentnotallowed', new lang_string('downloadcontentnotallowed', 'tool_uploadcourse')); 784 return false; 785 } 786 787 $downloadcontentvalues = [ 788 DOWNLOAD_COURSE_CONTENT_DISABLED, 789 DOWNLOAD_COURSE_CONTENT_ENABLED, 790 DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT, 791 ]; 792 if (!in_array($coursedata['downloadcontent'], $downloadcontentvalues)) { 793 $this->error('invaliddownloadcontent', new lang_string('invaliddownloadcontent', 'tool_uploadcourse')); 794 return false; 795 } 796 } 797 798 // Saving data. 799 $this->data = $coursedata; 800 801 // Get enrolment data. Where the course already exists, we can also perform validation. 802 $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata); 803 $courseid = $coursedata['id'] ?? 0; 804 $errors = $this->validate_enrolment_data($courseid, $this->enrolmentdata); 805 806 if (!empty($errors)) { 807 foreach ($errors as $key => $message) { 808 $this->error($key, $message); 809 } 810 811 return false; 812 } 813 814 if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') { 815 $this->data['tags'] = preg_split('/\s*,\s*/', trim($this->rawdata['tags']), -1, PREG_SPLIT_NO_EMPTY); 816 } 817 818 // Restore data. 819 // TODO Speed up things by not really extracting the backup just yet, but checking that 820 // the backup file or shortname passed are valid. Extraction should happen in proceed(). 821 $this->restoredata = $this->get_restore_content_dir(); 822 if ($this->restoredata === false) { 823 return false; 824 } 825 826 // We can only reset courses when allowed and we are updating the course. 827 if ($this->importoptions['reset'] || $this->options['reset']) { 828 if ($this->do !== self::DO_UPDATE) { 829 $this->error('canonlyresetcourseinupdatemode', 830 new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse')); 831 return false; 832 } else if (!$this->can_reset()) { 833 $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse')); 834 return false; 835 } 836 } 837 838 return true; 839 } 840 841 /** 842 * Proceed with the import of the course. 843 * 844 * @return void 845 */ 846 public function proceed() { 847 global $CFG, $USER; 848 849 if (!$this->prepared) { 850 throw new coding_exception('The course has not been prepared.'); 851 } else if ($this->has_errors()) { 852 throw new moodle_exception('Cannot proceed, errors were detected.'); 853 } else if ($this->processstarted) { 854 throw new coding_exception('The process has already been started.'); 855 } 856 $this->processstarted = true; 857 858 if ($this->do === self::DO_DELETE) { 859 if ($this->delete()) { 860 $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse')); 861 } else { 862 $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse')); 863 } 864 return true; 865 } else if ($this->do === self::DO_CREATE) { 866 $course = create_course((object) $this->data); 867 $this->id = $course->id; 868 $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse')); 869 } else if ($this->do === self::DO_UPDATE) { 870 $course = (object) $this->data; 871 update_course($course); 872 $this->id = $course->id; 873 $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse')); 874 } else { 875 // Strangely the outcome has not been defined, or is unknown! 876 throw new coding_exception('Unknown outcome!'); 877 } 878 879 // Restore a course. 880 if (!empty($this->restoredata)) { 881 $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO, 882 backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); 883 884 // Check if the format conversion must happen first. 885 if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) { 886 $rc->convert(); 887 } 888 if ($rc->execute_precheck()) { 889 $rc->execute_plan(); 890 $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse')); 891 } else { 892 $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse')); 893 } 894 $rc->destroy(); 895 } 896 897 // Proceed with enrolment data. 898 $this->process_enrolment_data($course); 899 900 // Reset the course. 901 if ($this->importoptions['reset'] || $this->options['reset']) { 902 if ($this->do === self::DO_UPDATE && $this->can_reset()) { 903 $this->reset($course); 904 $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse')); 905 } 906 } 907 908 // Mark context as dirty. 909 $context = context_course::instance($course->id); 910 $context->mark_dirty(); 911 } 912 913 /** 914 * Validate passed enrolment data against an existing course 915 * 916 * @param int $courseid 917 * @param array[] $enrolmentdata 918 * @return lang_string[] Errors keyed on error code 919 */ 920 protected function validate_enrolment_data(int $courseid, array $enrolmentdata): array { 921 global $DB; 922 923 // Nothing to validate. 924 if (empty($enrolmentdata)) { 925 return []; 926 } 927 928 $errors = []; 929 930 $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins(); 931 $instances = enrol_get_instances($courseid, false); 932 933 foreach ($enrolmentdata as $method => $options) { 934 935 if (isset($options['role'])) { 936 $role = $options['role']; 937 if ($courseid) { 938 if (!$this->validate_role_context($courseid, $role)) { 939 $errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role); 940 941 break; 942 } 943 } else { 944 // We can at least check that context level is correct while actual context not exist. 945 $roleid = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST); 946 if (!$this->validate_role_context_level($roleid)) { 947 $errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role); 948 949 break; 950 } 951 } 952 } 953 954 if ($courseid) { 955 $plugin = $enrolmentplugins[$method]; 956 957 // Find matching instances by enrolment method. 958 $methodinstances = array_filter($instances, static function (stdClass $instance) use ($method) { 959 return (strcmp($instance->enrol, $method) == 0); 960 }); 961 962 if (!empty($options['delete'])) { 963 // Ensure user is able to delete the instances. 964 foreach ($methodinstances as $methodinstance) { 965 if (!$plugin->can_delete_instance($methodinstance)) { 966 $errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment', 967 'tool_uploadcourse', $plugin->get_instance_name($methodinstance)); 968 break; 969 } 970 } 971 } else if (!empty($options['disable'])) { 972 // Ensure user is able to toggle instance statuses. 973 foreach ($methodinstances as $methodinstance) { 974 if (!$plugin->can_hide_show_instance($methodinstance)) { 975 $errors['errorcannotdisableenrolment'] = 976 new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse', 977 $plugin->get_instance_name($methodinstance)); 978 979 break; 980 } 981 } 982 } else { 983 // Ensure user is able to create/update instance. 984 $methodinstance = empty($methodinstances) ? null : reset($methodinstances); 985 if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) || 986 (!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) { 987 988 $errors['errorcannotcreateorupdateenrolment'] = 989 new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse', 990 $plugin->get_instance_name($methodinstance)); 991 992 break; 993 } 994 } 995 } 996 } 997 998 return $errors; 999 } 1000 1001 /** 1002 * Add the enrolment data for the course. 1003 * 1004 * @param object $course course record. 1005 * @return void 1006 */ 1007 protected function process_enrolment_data($course) { 1008 global $DB; 1009 1010 $enrolmentdata = $this->enrolmentdata; 1011 if (empty($enrolmentdata)) { 1012 return; 1013 } 1014 1015 $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins(); 1016 $instances = enrol_get_instances($course->id, false); 1017 foreach ($enrolmentdata as $enrolmethod => $method) { 1018 1019 $instance = null; 1020 foreach ($instances as $i) { 1021 if ($i->enrol == $enrolmethod) { 1022 $instance = $i; 1023 break; 1024 } 1025 } 1026 1027 $todelete = isset($method['delete']) && $method['delete']; 1028 $todisable = isset($method['disable']) && $method['disable']; 1029 unset($method['delete']); 1030 unset($method['disable']); 1031 1032 if ($todelete) { 1033 // Remove the enrolment method. 1034 if ($instance) { 1035 $plugin = $enrolmentplugins[$instance->enrol]; 1036 1037 // Ensure user is able to delete the instance. 1038 if ($plugin->can_delete_instance($instance)) { 1039 $plugin->delete_instance($instance); 1040 } else { 1041 $this->error('errorcannotdeleteenrolment', 1042 new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse', 1043 $plugin->get_instance_name($instance))); 1044 } 1045 } 1046 } else { 1047 // Create/update enrolment. 1048 $plugin = $enrolmentplugins[$enrolmethod]; 1049 1050 $status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED; 1051 1052 // Create a new instance if necessary. 1053 if (empty($instance) && $plugin->can_add_instance($course->id)) { 1054 $instanceid = $plugin->add_default_instance($course); 1055 $instance = $DB->get_record('enrol', ['id' => $instanceid]); 1056 $instance->roleid = $plugin->get_config('roleid'); 1057 // On creation the user can decide the status. 1058 $plugin->update_status($instance, $status); 1059 } 1060 1061 // Check if the we need to update the instance status. 1062 if ($instance && $status != $instance->status) { 1063 if ($plugin->can_hide_show_instance($instance)) { 1064 $plugin->update_status($instance, $status); 1065 } else { 1066 $this->error('errorcannotdisableenrolment', 1067 new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse', 1068 $plugin->get_instance_name($instance))); 1069 break; 1070 } 1071 } 1072 1073 if (empty($instance) || !$plugin->can_edit_instance($instance)) { 1074 $this->error('errorcannotcreateorupdateenrolment', 1075 new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse', 1076 $plugin->get_instance_name($instance))); 1077 1078 break; 1079 } 1080 1081 // Now update values. 1082 foreach ($method as $k => $v) { 1083 $instance->{$k} = $v; 1084 } 1085 1086 // Sort out the start, end and date. 1087 $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0); 1088 $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0); 1089 1090 // Is the enrolment period set? 1091 if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) { 1092 if (preg_match('/^\d+$/', $method['enrolperiod'])) { 1093 $method['enrolperiod'] = (int) $method['enrolperiod']; 1094 } else { 1095 // Try and convert period to seconds. 1096 $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']); 1097 } 1098 $instance->enrolperiod = $method['enrolperiod']; 1099 } 1100 if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) { 1101 $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod']; 1102 } 1103 if ($instance->enrolenddate > 0) { 1104 $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate; 1105 } 1106 if ($instance->enrolenddate < $instance->enrolstartdate) { 1107 $instance->enrolenddate = $instance->enrolstartdate; 1108 } 1109 1110 // Sort out the given role. 1111 if (isset($method['role'])) { 1112 $role = $method['role']; 1113 if (!$this->validate_role_context($course->id, $role)) { 1114 $this->error('contextrolenotallowed', 1115 new lang_string('contextrolenotallowed', 'core_role', $role)); 1116 break; 1117 } 1118 1119 $roleids = tool_uploadcourse_helper::get_role_ids(); 1120 if (isset($roleids[$method['role']])) { 1121 $instance->roleid = $roleids[$method['role']]; 1122 } 1123 } 1124 1125 $instance->timemodified = time(); 1126 $DB->update_record('enrol', $instance); 1127 } 1128 } 1129 } 1130 1131 /** 1132 * Check if role is allowed in course context 1133 * 1134 * @param int $courseid course context. 1135 * @param string $role Role. 1136 * @return bool 1137 */ 1138 protected function validate_role_context(int $courseid, string $role) : bool { 1139 if (empty($this->assignableroles[$courseid])) { 1140 $coursecontext = \context_course::instance($courseid); 1141 $this->assignableroles[$courseid] = get_assignable_roles($coursecontext, ROLENAME_SHORT); 1142 } 1143 if (!in_array($role, $this->assignableroles[$courseid])) { 1144 return false; 1145 } 1146 return true; 1147 } 1148 1149 /** 1150 * Check if role is allowed at this context level. 1151 * 1152 * @param int $roleid Role ID. 1153 * @return bool 1154 */ 1155 protected function validate_role_context_level(int $roleid) : bool { 1156 if (empty($this->contextlevels[$roleid])) { 1157 $this->contextlevels[$roleid] = get_role_contextlevels($roleid); 1158 } 1159 1160 if (!in_array(CONTEXT_COURSE, $this->contextlevels[$roleid])) { 1161 return false; 1162 } 1163 return true; 1164 } 1165 1166 /** 1167 * Reset the current course. 1168 * 1169 * This does not reset any of the content of the activities. 1170 * 1171 * @param stdClass $course the course object of the course to reset. 1172 * @return array status array of array component, item, error. 1173 */ 1174 protected function reset($course) { 1175 global $DB; 1176 1177 $resetdata = new stdClass(); 1178 $resetdata->id = $course->id; 1179 $resetdata->reset_start_date = time(); 1180 $resetdata->reset_events = true; 1181 $resetdata->reset_notes = true; 1182 $resetdata->delete_blog_associations = true; 1183 $resetdata->reset_completion = true; 1184 $resetdata->reset_roles_overrides = true; 1185 $resetdata->reset_roles_local = true; 1186 $resetdata->reset_groups_members = true; 1187 $resetdata->reset_groups_remove = true; 1188 $resetdata->reset_groupings_members = true; 1189 $resetdata->reset_groupings_remove = true; 1190 $resetdata->reset_gradebook_items = true; 1191 $resetdata->reset_gradebook_grades = true; 1192 $resetdata->reset_comments = true; 1193 1194 if (empty($course->startdate)) { 1195 $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id)); 1196 } 1197 $resetdata->reset_start_date_old = $course->startdate; 1198 1199 if (empty($course->enddate)) { 1200 $course->enddate = $DB->get_field_select('course', 'enddate', 'id = :id', array('id' => $course->id)); 1201 } 1202 $resetdata->reset_end_date_old = $course->enddate; 1203 1204 // Add roles. 1205 $roles = tool_uploadcourse_helper::get_role_ids(); 1206 $resetdata->unenrol_users = array_values($roles); 1207 $resetdata->unenrol_users[] = 0; // Enrolled without role. 1208 1209 return reset_course_userdata($resetdata); 1210 } 1211 1212 /** 1213 * Log a status 1214 * 1215 * @param string $code status code. 1216 * @param lang_string $message status message. 1217 * @return void 1218 */ 1219 protected function status($code, lang_string $message) { 1220 if (array_key_exists($code, $this->statuses)) { 1221 throw new coding_exception('Status code already defined'); 1222 } 1223 $this->statuses[$code] = $message; 1224 } 1225 1226 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body