Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [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  namespace core_courseformat;
  18  
  19  use core\event\course_module_updated;
  20  use core_courseformat\base as course_format;
  21  use core_courseformat\stateupdates;
  22  use cm_info;
  23  use section_info;
  24  use stdClass;
  25  use course_modinfo;
  26  use moodle_exception;
  27  use context_module;
  28  use context_course;
  29  use cache;
  30  
  31  /**
  32   * Contains the core course state actions.
  33   *
  34   * The methods from this class should be executed via "core_courseformat_edit" web service.
  35   *
  36   * Each format plugin could extend this class to provide new actions to the editor.
  37   * Extended classes should be locate in "format_XXX\course" namespace and
  38   * extends core_courseformat\stateactions.
  39   *
  40   * @package    core_courseformat
  41   * @copyright  2021 Ferran Recio <ferran@moodle.com>
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class stateactions {
  45  
  46      /**
  47       * Move course modules to another location in the same course.
  48       *
  49       * @param stateupdates $updates the affected course elements track
  50       * @param stdClass $course the course object
  51       * @param int[] $ids the list of affected course module ids
  52       * @param int $targetsectionid optional target section id
  53       * @param int $targetcmid optional target cm id
  54       */
  55      public function cm_move(
  56          stateupdates $updates,
  57          stdClass $course,
  58          array $ids,
  59          ?int $targetsectionid = null,
  60          ?int $targetcmid = null
  61      ): void {
  62          // Validate target elements.
  63          if (!$targetsectionid && !$targetcmid) {
  64              throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid");
  65          }
  66  
  67          $this->validate_cms($course, $ids, __FUNCTION__);
  68  
  69          // Check capabilities on every activity context.
  70          foreach ($ids as $cmid) {
  71              $modcontext = context_module::instance($cmid);
  72              require_capability('moodle/course:manageactivities', $modcontext);
  73          }
  74  
  75          $modinfo = get_fast_modinfo($course);
  76  
  77          // Target cm has more priority than target section.
  78          if (!empty($targetcmid)) {
  79              $this->validate_cms($course, [$targetcmid], __FUNCTION__);
  80              $targetcm = $modinfo->get_cm($targetcmid);
  81              $targetsection = $modinfo->get_section_info_by_id($targetcm->section, MUST_EXIST);
  82          } else {
  83              $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
  84              $targetcm = null;
  85              $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
  86          }
  87  
  88          // The origin sections must be updated as well.
  89          $originalsections = [];
  90  
  91          $cms = $this->get_cm_info($modinfo, $ids);
  92          foreach ($cms as $cm) {
  93              $currentsection = $modinfo->get_section_info_by_id($cm->section, MUST_EXIST);
  94              moveto_module($cm, $targetsection, $targetcm);
  95              $updates->add_cm_put($cm->id);
  96              if ($currentsection->id != $targetsection->id) {
  97                  $originalsections[$currentsection->id] = true;
  98              }
  99              // If some of the original sections are also target sections, we don't need to update them.
 100              if (array_key_exists($targetsection->id, $originalsections)) {
 101                  unset($originalsections[$targetsection->id]);
 102              }
 103          }
 104  
 105          // Use section_state to return the full affected section and activities updated state.
 106          $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid);
 107  
 108          foreach (array_keys($originalsections) as $sectionid) {
 109              $updates->add_section_put($sectionid);
 110          }
 111      }
 112  
 113      /**
 114       * Move course sections to another location in the same course.
 115       *
 116       * @param stateupdates $updates the affected course elements track
 117       * @param stdClass $course the course object
 118       * @param int[] $ids the list of affected course module ids
 119       * @param int $targetsectionid optional target section id
 120       * @param int $targetcmid optional target cm id
 121       */
 122      public function section_move(
 123          stateupdates $updates,
 124          stdClass $course,
 125          array $ids,
 126          ?int $targetsectionid = null,
 127          ?int $targetcmid = null
 128      ): void {
 129          // Validate target elements.
 130          if (!$targetsectionid) {
 131              throw new moodle_exception("Action cm_move requires targetsectionid");
 132          }
 133  
 134          $this->validate_sections($course, $ids, __FUNCTION__);
 135  
 136          $coursecontext = context_course::instance($course->id);
 137          require_capability('moodle/course:movesections', $coursecontext);
 138  
 139          $modinfo = get_fast_modinfo($course);
 140  
 141          // Target section.
 142          $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 143          $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
 144  
 145          $affectedsections = [$targetsection->section => true];
 146  
 147          $sections = $this->get_section_info($modinfo, $ids);
 148          foreach ($sections as $section) {
 149              $affectedsections[$section->section] = true;
 150              move_section_to($course, $section->section, $targetsection->section);
 151          }
 152  
 153          // Use section_state to return the section and activities updated state.
 154          $this->section_state($updates, $course, $ids, $targetsectionid);
 155  
 156          // All course sections can be renamed because of the resort.
 157          $allsections = $modinfo->get_section_info_all();
 158          foreach ($allsections as $section) {
 159              // Ignore the affected sections because they are already in the updates.
 160              if (isset($affectedsections[$section->section])) {
 161                  continue;
 162              }
 163              $updates->add_section_put($section->id);
 164          }
 165          // The section order is at a course level.
 166          $updates->add_course_put();
 167      }
 168  
 169      /**
 170       * Create a course section.
 171       *
 172       * This method follows the same logic as changenumsections.php.
 173       *
 174       * @param stateupdates $updates the affected course elements track
 175       * @param stdClass $course the course object
 176       * @param int[] $ids not used
 177       * @param int $targetsectionid optional target section id (if not passed section will be appended)
 178       * @param int $targetcmid not used
 179       */
 180      public function section_add(
 181          stateupdates $updates,
 182          stdClass $course,
 183          array $ids = [],
 184          ?int $targetsectionid = null,
 185          ?int $targetcmid = null
 186      ): void {
 187  
 188          $coursecontext = context_course::instance($course->id);
 189          require_capability('moodle/course:update', $coursecontext);
 190  
 191          // Get course format settings.
 192          $format = course_get_format($course->id);
 193          $lastsectionnumber = $format->get_last_section_number();
 194          $maxsections = $format->get_max_sections();
 195  
 196          if ($lastsectionnumber >= $maxsections) {
 197              throw new moodle_exception('maxsectionslimit', 'moodle', $maxsections);
 198          }
 199  
 200          $modinfo = get_fast_modinfo($course);
 201  
 202          // Get target section.
 203          if ($targetsectionid) {
 204              $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 205              $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
 206              // Inserting sections at any position except in the very end requires capability to move sections.
 207              require_capability('moodle/course:movesections', $coursecontext);
 208              $insertposition = $targetsection->section + 1;
 209          } else {
 210              // Get last section.
 211              $insertposition = 0;
 212          }
 213  
 214          course_create_section($course, $insertposition);
 215  
 216          // Adding a section affects the full course structure.
 217          $this->course_state($updates, $course);
 218      }
 219  
 220      /**
 221       * Delete course sections.
 222       *
 223       * This method follows the same logic as editsection.php.
 224       *
 225       * @param stateupdates $updates the affected course elements track
 226       * @param stdClass $course the course object
 227       * @param int[] $ids section ids
 228       * @param int $targetsectionid not used
 229       * @param int $targetcmid not used
 230       */
 231      public function section_delete(
 232          stateupdates $updates,
 233          stdClass $course,
 234          array $ids = [],
 235          ?int $targetsectionid = null,
 236          ?int $targetcmid = null
 237      ): void {
 238  
 239          $coursecontext = context_course::instance($course->id);
 240          require_capability('moodle/course:update', $coursecontext);
 241          require_capability('moodle/course:movesections', $coursecontext);
 242  
 243          $modinfo = get_fast_modinfo($course);
 244  
 245          foreach ($ids as $sectionid) {
 246              $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
 247              // Send all activity deletions.
 248              if (!empty($modinfo->sections[$section->section])) {
 249                  foreach ($modinfo->sections[$section->section] as $modnumber) {
 250                      $cm = $modinfo->cms[$modnumber];
 251                      $updates->add_cm_remove($cm->id);
 252                  }
 253              }
 254              course_delete_section($course, $section, true, true);
 255              $updates->add_section_remove($sectionid);
 256          }
 257  
 258          // Removing a section affects the full course structure.
 259          $this->course_state($updates, $course);
 260      }
 261  
 262      /**
 263       * Move course cms to the right. Indent = 1.
 264       *
 265       * @param stateupdates $updates the affected course elements track
 266       * @param stdClass $course the course object
 267       * @param int[] $ids cm ids
 268       * @param int $targetsectionid not used
 269       * @param int $targetcmid not used
 270       */
 271      public function cm_moveright(
 272          stateupdates $updates,
 273          stdClass $course,
 274          array $ids = [],
 275          ?int $targetsectionid = null,
 276          ?int $targetcmid = null
 277      ): void {
 278          $this->set_cm_indentation($updates, $course, $ids, 1);
 279      }
 280  
 281      /**
 282       * Move course cms to the left. Indent = 0.
 283       *
 284       * @param stateupdates $updates the affected course elements track
 285       * @param stdClass $course the course object
 286       * @param int[] $ids cm ids
 287       * @param int $targetsectionid not used
 288       * @param int $targetcmid not used
 289       */
 290      public function cm_moveleft(
 291          stateupdates $updates,
 292          stdClass $course,
 293          array $ids = [],
 294          ?int $targetsectionid = null,
 295          ?int $targetcmid = null
 296      ): void {
 297          $this->set_cm_indentation($updates, $course, $ids, 0);
 298      }
 299  
 300      /**
 301       * Internal method to define the cm indentation level.
 302       *
 303       * @param stateupdates $updates the affected course elements track
 304       * @param stdClass $course the course object
 305       * @param int[] $ids cm ids
 306       * @param int $indent new value for indentation
 307       */
 308      protected function set_cm_indentation(
 309          stateupdates $updates,
 310          stdClass $course,
 311          array $ids,
 312          int $indent
 313      ): void {
 314          global $DB;
 315  
 316          $this->validate_cms($course, $ids, __FUNCTION__);
 317  
 318          // Check capabilities on every activity context.
 319          foreach ($ids as $cmid) {
 320              $modcontext = context_module::instance($cmid);
 321              require_capability('moodle/course:manageactivities', $modcontext);
 322          }
 323          $modinfo = get_fast_modinfo($course);
 324          $cms = $this->get_cm_info($modinfo, $ids);
 325          list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED);
 326          $DB->set_field_select('course_modules', 'indent', $indent, "id $insql", $inparams);
 327          rebuild_course_cache($course->id, false, true);
 328          foreach ($cms as $cm) {
 329              $modcontext = context_module::instance($cm->id);
 330              course_module_updated::create_from_cm($cm, $modcontext)->trigger();
 331              $updates->add_cm_put($cm->id);
 332          }
 333      }
 334  
 335      /**
 336       * Extract several cm_info from the course_modinfo.
 337       *
 338       * @param course_modinfo $modinfo the course modinfo.
 339       * @param int[] $ids the course modules $ids
 340       * @return cm_info[] the extracted cm_info objects
 341       */
 342      protected function get_cm_info (course_modinfo $modinfo, array $ids): array {
 343          $cms = [];
 344          foreach ($ids as $cmid) {
 345              $cms[$cmid] = $modinfo->get_cm($cmid);
 346          }
 347          return $cms;
 348      }
 349  
 350      /**
 351       * Extract several section_info from the course_modinfo.
 352       *
 353       * @param course_modinfo $modinfo the course modinfo.
 354       * @param int[] $ids the course modules $ids
 355       * @return section_info[] the extracted section_info objects
 356       */
 357      protected function get_section_info(course_modinfo $modinfo, array $ids): array {
 358          $sections = [];
 359          foreach ($ids as $sectionid) {
 360              $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid);
 361          }
 362          return $sections;
 363      }
 364  
 365      /**
 366       * Update the course content section collapsed value.
 367       *
 368       * @param stateupdates $updates the affected course elements track
 369       * @param stdClass $course the course object
 370       * @param int[] $ids the collapsed section ids
 371       * @param int $targetsectionid not used
 372       * @param int $targetcmid not used
 373       */
 374      public function section_content_collapsed(
 375          stateupdates $updates,
 376          stdClass $course,
 377          array $ids = [],
 378          ?int $targetsectionid = null,
 379          ?int $targetcmid = null
 380      ): void {
 381          if (!empty($ids)) {
 382              $this->validate_sections($course, $ids, __FUNCTION__);
 383          }
 384          $format = course_get_format($course->id);
 385          $format->set_sections_preference('contentcollapsed', $ids);
 386      }
 387  
 388      /**
 389       * Update the course index section collapsed value.
 390       *
 391       * @param stateupdates $updates the affected course elements track
 392       * @param stdClass $course the course object
 393       * @param int[] $ids the collapsed section ids
 394       * @param int $targetsectionid not used
 395       * @param int $targetcmid not used
 396       */
 397      public function section_index_collapsed(
 398          stateupdates $updates,
 399          stdClass $course,
 400          array $ids = [],
 401          ?int $targetsectionid = null,
 402          ?int $targetcmid = null
 403      ): void {
 404          if (!empty($ids)) {
 405              $this->validate_sections($course, $ids, __FUNCTION__);
 406          }
 407          $format = course_get_format($course->id);
 408          $format->set_sections_preference('indexcollapsed', $ids);
 409      }
 410  
 411      /**
 412       * Add the update messages of the updated version of any cm and section related to the cm ids.
 413       *
 414       * This action is mainly used by legacy actions to partially update the course state when the
 415       * result of core_course_edit_module is not enough to generate the correct state data.
 416       *
 417       * @param stateupdates $updates the affected course elements track
 418       * @param stdClass $course the course object
 419       * @param int[] $ids the list of affected course module ids
 420       * @param int $targetsectionid optional target section id
 421       * @param int $targetcmid optional target cm id
 422       */
 423      public function cm_state(
 424          stateupdates $updates,
 425          stdClass $course,
 426          array $ids,
 427          ?int $targetsectionid = null,
 428          ?int $targetcmid = null
 429      ): void {
 430  
 431          // Collect all section and cm to return.
 432          $cmids = [];
 433          foreach ($ids as $cmid) {
 434              $cmids[$cmid] = true;
 435          }
 436          if ($targetcmid) {
 437              $cmids[$targetcmid] = true;
 438          }
 439  
 440          $sectionids = [];
 441          if ($targetsectionid) {
 442              $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 443              $sectionids[$targetsectionid] = true;
 444          }
 445  
 446          $this->validate_cms($course, array_keys($cmids), __FUNCTION__);
 447  
 448          $modinfo = course_modinfo::instance($course);
 449  
 450          foreach (array_keys($cmids) as $cmid) {
 451  
 452              // Add this action to updates array.
 453              $updates->add_cm_put($cmid);
 454  
 455              $cm = $modinfo->get_cm($cmid);
 456              $sectionids[$cm->section] = true;
 457          }
 458  
 459          foreach (array_keys($sectionids) as $sectionid) {
 460              $updates->add_section_put($sectionid);
 461          }
 462      }
 463  
 464      /**
 465       * Add the update messages of the updated version of any cm and section related to the section ids.
 466       *
 467       * This action is mainly used by legacy actions to partially update the course state when the
 468       * result of core_course_edit_module is not enough to generate the correct state data.
 469       *
 470       * @param stateupdates $updates the affected course elements track
 471       * @param stdClass $course the course object
 472       * @param int[] $ids the list of affected course section ids
 473       * @param int $targetsectionid optional target section id
 474       * @param int $targetcmid optional target cm id
 475       */
 476      public function section_state(
 477          stateupdates $updates,
 478          stdClass $course,
 479          array $ids,
 480          ?int $targetsectionid = null,
 481          ?int $targetcmid = null
 482      ): void {
 483  
 484          $cmids = [];
 485          if ($targetcmid) {
 486              $this->validate_cms($course, [$targetcmid], __FUNCTION__);
 487              $cmids[$targetcmid] = true;
 488          }
 489  
 490          $sectionids = [];
 491          foreach ($ids as $sectionid) {
 492              $sectionids[$sectionid] = true;
 493          }
 494          if ($targetsectionid) {
 495              $sectionids[$targetsectionid] = true;
 496          }
 497  
 498          $this->validate_sections($course, array_keys($sectionids), __FUNCTION__);
 499  
 500          $modinfo = course_modinfo::instance($course);
 501  
 502          foreach (array_keys($sectionids) as $sectionid) {
 503              $sectioninfo = $modinfo->get_section_info_by_id($sectionid);
 504              $updates->add_section_put($sectionid);
 505              // Add cms.
 506              if (empty($modinfo->sections[$sectioninfo->section])) {
 507                  continue;
 508              }
 509  
 510              foreach ($modinfo->sections[$sectioninfo->section] as $modnumber) {
 511                  $mod = $modinfo->cms[$modnumber];
 512                  if ($mod->is_visible_on_course_page()) {
 513                      $cmids[$mod->id] = true;
 514                  }
 515              }
 516          }
 517  
 518          foreach (array_keys($cmids) as $cmid) {
 519              // Add this action to updates array.
 520              $updates->add_cm_put($cmid);
 521          }
 522      }
 523  
 524      /**
 525       * Add all the update messages from the complete course state.
 526       *
 527       * This action is mainly used by legacy actions to partially update the course state when the
 528       * result of core_course_edit_module is not enough to generate the correct state data.
 529       *
 530       * @param stateupdates $updates the affected course elements track
 531       * @param stdClass $course the course object
 532       * @param int[] $ids the list of affected course module ids (not used)
 533       * @param int $targetsectionid optional target section id (not used)
 534       * @param int $targetcmid optional target cm id (not used)
 535       */
 536      public function course_state(
 537          stateupdates $updates,
 538          stdClass $course,
 539          array $ids = [],
 540          ?int $targetsectionid = null,
 541          ?int $targetcmid = null
 542      ): void {
 543  
 544          $modinfo = course_modinfo::instance($course);
 545  
 546          $updates->add_course_put();
 547  
 548          // Add sections updates.
 549          $sections = $modinfo->get_section_info_all();
 550          $sectionids = [];
 551          foreach ($sections as $sectioninfo) {
 552              $sectionids[] = $sectioninfo->id;
 553          }
 554          if (!empty($sectionids)) {
 555              $this->section_state($updates, $course, $sectionids);
 556          }
 557      }
 558  
 559      /**
 560       * Checks related to sections: course format support them, all given sections exist and topic 0 is not included.
 561       *
 562       * @param stdClass $course The course where given $sectionids belong.
 563       * @param array $sectionids List of sections to validate.
 564       * @param string|null $info additional information in case of error (default null).
 565       * @throws moodle_exception if any id is not valid
 566       */
 567      protected function validate_sections(stdClass $course, array $sectionids, ?string $info = null): void {
 568          global $DB;
 569  
 570          if (empty($sectionids)) {
 571              throw new moodle_exception('emptysectionids', 'core', null, $info);
 572          }
 573  
 574          // No section actions are allowed if course format does not support sections.
 575          $courseformat = course_get_format($course->id);
 576          if (!$courseformat->uses_sections()) {
 577              throw new moodle_exception('sectionactionnotsupported', 'core', null, $info);
 578          }
 579  
 580          list($insql, $inparams) = $DB->get_in_or_equal($sectionids, SQL_PARAMS_NAMED);
 581  
 582          // Check if all the given sections exist.
 583          $couintsections = $DB->count_records_select('course_sections', "id $insql", $inparams);
 584          if ($couintsections != count($sectionids)) {
 585              throw new moodle_exception('unexistingsectionid', 'core', null, $info);
 586          }
 587      }
 588  
 589      /**
 590       * Checks related to course modules: all given cm exist.
 591       *
 592       * @param stdClass $course The course where given $cmids belong.
 593       * @param array $cmids List of course module ids to validate.
 594       * @param string $info additional information in case of error.
 595       * @throws moodle_exception if any id is not valid
 596       */
 597      protected function validate_cms(stdClass $course, array $cmids, ?string $info = null): void {
 598  
 599          if (empty($cmids)) {
 600              throw new moodle_exception('emptycmids', 'core', null, $info);
 601          }
 602  
 603          $moduleinfo = get_fast_modinfo($course->id);
 604          $intersect = array_intersect($cmids, array_keys($moduleinfo->get_cms()));
 605          if (count($cmids) != count($intersect)) {
 606              throw new moodle_exception('unexistingcmid', 'core', null, $info);
 607          }
 608      }
 609  }