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