Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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', 'showactivitydates'); 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']) || isset($options['roleid'])) { 936 if (isset($options['role'])) { 937 $role = $options['role']; 938 $roleid = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST); 939 } else { 940 $roleid = $options['roleid']; 941 $role = $DB->get_field('role', 'shortname', ['id' => $roleid], MUST_EXIST); 942 } 943 if ($courseid) { 944 if (!$this->validate_role_context($courseid, $roleid)) { 945 $errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role); 946 947 break; 948 } 949 } else { 950 // We can at least check that context level is correct while actual context not exist. 951 if (!$this->validate_role_context_level($roleid)) { 952 $errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role); 953 954 break; 955 } 956 } 957 } 958 959 $plugin = $enrolmentplugins[$method]; 960 $errors += $plugin->validate_enrol_plugin_data($options, $courseid); 961 if ($errors) { 962 break; 963 } 964 965 if ($courseid) { 966 // Find matching instances by enrolment method. 967 $methodinstances = array_filter($instances, static function (stdClass $instance) use ($method) { 968 return (strcmp($instance->enrol, $method) == 0); 969 }); 970 971 if (!empty($options['delete'])) { 972 // Ensure user is able to delete the instances. 973 foreach ($methodinstances as $methodinstance) { 974 if (!$plugin->can_delete_instance($methodinstance)) { 975 $errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment', 976 'tool_uploadcourse', $plugin->get_instance_name($methodinstance)); 977 break; 978 } 979 } 980 } else if (!empty($options['disable'])) { 981 // Ensure user is able to toggle instance statuses. 982 foreach ($methodinstances as $methodinstance) { 983 if (!$plugin->can_hide_show_instance($methodinstance)) { 984 $errors['errorcannotdisableenrolment'] = 985 new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse', 986 $plugin->get_instance_name($methodinstance)); 987 988 break; 989 } 990 } 991 } else { 992 // Ensure user is able to create/update instance. 993 $methodinstance = empty($methodinstances) ? null : reset($methodinstances); 994 if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) || 995 (!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) { 996 997 $errors['errorcannotcreateorupdateenrolment'] = 998 new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse', 999 $plugin->get_instance_name($methodinstance)); 1000 1001 break; 1002 } 1003 } 1004 } 1005 } 1006 1007 return $errors; 1008 } 1009 1010 /** 1011 * Add the enrolment data for the course. 1012 * 1013 * @param object $course course record. 1014 * @return void 1015 */ 1016 protected function process_enrolment_data($course) { 1017 global $DB; 1018 1019 $enrolmentdata = $this->enrolmentdata; 1020 if (empty($enrolmentdata)) { 1021 return; 1022 } 1023 1024 $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins(); 1025 $instances = enrol_get_instances($course->id, false); 1026 foreach ($enrolmentdata as $enrolmethod => $method) { 1027 1028 $instance = null; 1029 foreach ($instances as $i) { 1030 if ($i->enrol == $enrolmethod) { 1031 $instance = $i; 1032 break; 1033 } 1034 } 1035 1036 $todelete = isset($method['delete']) && $method['delete']; 1037 $todisable = isset($method['disable']) && $method['disable']; 1038 unset($method['delete']); 1039 unset($method['disable']); 1040 1041 if ($todelete) { 1042 // Remove the enrolment method. 1043 if ($instance) { 1044 $plugin = $enrolmentplugins[$instance->enrol]; 1045 1046 // Ensure user is able to delete the instance. 1047 if ($plugin->can_delete_instance($instance) && $plugin->is_csv_upload_supported()) { 1048 $plugin->delete_instance($instance); 1049 } else { 1050 $this->error('errorcannotdeleteenrolment', 1051 new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse', 1052 $plugin->get_instance_name($instance))); 1053 } 1054 } 1055 } else { 1056 // Create/update enrolment. 1057 $plugin = $enrolmentplugins[$enrolmethod]; 1058 1059 if ($plugin->is_csv_upload_supported()) { 1060 $status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED; 1061 $method = $plugin->fill_enrol_custom_fields($method, $course->id); 1062 1063 // Create a new instance if necessary. 1064 if (empty($instance) && $plugin->can_add_instance($course->id)) { 1065 $error = $plugin->validate_plugin_data_context($method, $course->id); 1066 if ($error) { 1067 $this->error('contextnotallowed', $error); 1068 break; 1069 } 1070 $instanceid = $plugin->add_default_instance($course); 1071 if (!$instanceid) { 1072 // Add instance with provided fields if plugin supports it. 1073 $instanceid = $plugin->add_custom_instance($course, $method); 1074 } 1075 1076 $instance = $DB->get_record('enrol', ['id' => $instanceid]); 1077 if ($instance) { 1078 $instance->roleid = $plugin->get_config('roleid'); 1079 // On creation the user can decide the status. 1080 $plugin->update_status($instance, $status); 1081 } 1082 } 1083 1084 // Check if the we need to update the instance status. 1085 if ($instance && $status != $instance->status) { 1086 if ($plugin->can_hide_show_instance($instance)) { 1087 $plugin->update_status($instance, $status); 1088 } else { 1089 $this->error('errorcannotdisableenrolment', 1090 new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse', 1091 $plugin->get_instance_name($instance))); 1092 break; 1093 } 1094 } 1095 1096 if (empty($instance) || !$plugin->can_edit_instance($instance)) { 1097 $this->error('errorcannotcreateorupdateenrolment', 1098 new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse', 1099 $plugin->get_instance_name($instance))); 1100 1101 break; 1102 } 1103 1104 // Now update values. 1105 $modifiedinstance = $instance; 1106 1107 // Sort out the start, end and date. 1108 $modifiedinstance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0); 1109 $modifiedinstance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0); 1110 1111 // Is the enrolment period set? 1112 if (isset($method['enrolperiod']) && !empty($method['enrolperiod'])) { 1113 if (preg_match('/^\d+$/', $method['enrolperiod'])) { 1114 $method['enrolperiod'] = (int)$method['enrolperiod']; 1115 } else { 1116 // Try and convert period to seconds. 1117 $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']); 1118 } 1119 $modifiedinstance->enrolperiod = $method['enrolperiod']; 1120 } 1121 if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) { 1122 $modifiedinstance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod']; 1123 } 1124 if ($instance->enrolenddate > 0) { 1125 $modifiedinstance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate; 1126 } 1127 if ($instance->enrolenddate < $instance->enrolstartdate) { 1128 $modifiedinstance->enrolenddate = $instance->enrolstartdate; 1129 } 1130 1131 // Sort out the given role. 1132 if (isset($method['role']) || isset($method['roleid'])) { 1133 if (isset($method['role'])) { 1134 $role = $method['role']; 1135 $roleid = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST); 1136 } else { 1137 $roleid = $method['roleid']; 1138 $role = $DB->get_field('role', 'shortname', ['id' => $roleid], MUST_EXIST); 1139 } 1140 if (!$this->validate_role_context($course->id, $roleid)) { 1141 $this->error('contextrolenotallowed', 1142 new lang_string('contextrolenotallowed', 'core_role', $role)); 1143 break; 1144 } 1145 1146 $roleids = tool_uploadcourse_helper::get_role_ids(); 1147 if (in_array($roleid, $roleids)) { 1148 $modifiedinstance->roleid = $roleid; 1149 } 1150 } 1151 1152 // Sort out custom instance name. 1153 if (isset($method['name'])) { 1154 $modifiedinstance->name = $method['name']; 1155 } 1156 1157 $plugin->update_instance($instance, $modifiedinstance); 1158 } else { 1159 $this->error('errorunsupportedmethod', 1160 new lang_string('errorunsupportedmethod', 'tool_uploadcourse', 1161 $enrolmethod)); 1162 } 1163 } 1164 } 1165 } 1166 1167 /** 1168 * Check if role is allowed in course context 1169 * 1170 * @param int $courseid course context. 1171 * @param int $roleid Role ID. 1172 * @return bool 1173 */ 1174 protected function validate_role_context(int $courseid, int $roleid) : bool { 1175 if (empty($this->assignableroles[$courseid])) { 1176 $coursecontext = \context_course::instance($courseid); 1177 $this->assignableroles[$courseid] = get_assignable_roles($coursecontext, ROLENAME_SHORT); 1178 } 1179 if (!array_key_exists($roleid, $this->assignableroles[$courseid])) { 1180 return false; 1181 } 1182 return true; 1183 } 1184 1185 /** 1186 * Check if role is allowed at this context level. 1187 * 1188 * @param int $roleid Role ID. 1189 * @return bool 1190 */ 1191 protected function validate_role_context_level(int $roleid) : bool { 1192 if (empty($this->contextlevels[$roleid])) { 1193 $this->contextlevels[$roleid] = get_role_contextlevels($roleid); 1194 } 1195 1196 if (!in_array(CONTEXT_COURSE, $this->contextlevels[$roleid])) { 1197 return false; 1198 } 1199 return true; 1200 } 1201 1202 /** 1203 * Reset the current course. 1204 * 1205 * This does not reset any of the content of the activities. 1206 * 1207 * @param stdClass $course the course object of the course to reset. 1208 * @return array status array of array component, item, error. 1209 */ 1210 protected function reset($course) { 1211 global $DB; 1212 1213 $resetdata = new stdClass(); 1214 $resetdata->id = $course->id; 1215 $resetdata->reset_start_date = time(); 1216 $resetdata->reset_events = true; 1217 $resetdata->reset_notes = true; 1218 $resetdata->delete_blog_associations = true; 1219 $resetdata->reset_completion = true; 1220 $resetdata->reset_roles_overrides = true; 1221 $resetdata->reset_roles_local = true; 1222 $resetdata->reset_groups_members = true; 1223 $resetdata->reset_groups_remove = true; 1224 $resetdata->reset_groupings_members = true; 1225 $resetdata->reset_groupings_remove = true; 1226 $resetdata->reset_gradebook_items = true; 1227 $resetdata->reset_gradebook_grades = true; 1228 $resetdata->reset_comments = true; 1229 1230 if (empty($course->startdate)) { 1231 $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id)); 1232 } 1233 $resetdata->reset_start_date_old = $course->startdate; 1234 1235 if (empty($course->enddate)) { 1236 $course->enddate = $DB->get_field_select('course', 'enddate', 'id = :id', array('id' => $course->id)); 1237 } 1238 $resetdata->reset_end_date_old = $course->enddate; 1239 1240 // Add roles. 1241 $roles = tool_uploadcourse_helper::get_role_ids(); 1242 $resetdata->unenrol_users = array_values($roles); 1243 $resetdata->unenrol_users[] = 0; // Enrolled without role. 1244 1245 return reset_course_userdata($resetdata); 1246 } 1247 1248 /** 1249 * Log a status 1250 * 1251 * @param string $code status code. 1252 * @param lang_string $message status message. 1253 * @return void 1254 */ 1255 protected function status($code, lang_string $message) { 1256 if (array_key_exists($code, $this->statuses)) { 1257 throw new coding_exception('Status code already defined'); 1258 } 1259 $this->statuses[$code] = $message; 1260 } 1261 1262 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body