Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [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  namespace core_courseformat;
  18  
  19  use core\event\course_module_updated;
  20  use cm_info;
  21  use section_info;
  22  use stdClass;
  23  use course_modinfo;
  24  use moodle_exception;
  25  use context_module;
  26  use context_course;
  27  
  28  /**
  29   * Contains the core course state actions.
  30   *
  31   * The methods from this class should be executed via "core_courseformat_edit" web service.
  32   *
  33   * Each format plugin could extend this class to provide new actions to the editor.
  34   * Extended classes should be locate in "format_XXX\course" namespace and
  35   * extends core_courseformat\stateactions.
  36   *
  37   * @package    core_courseformat
  38   * @copyright  2021 Ferran Recio <ferran@moodle.com>
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class stateactions {
  42  
  43      /**
  44       * Move course modules to another location in the same course.
  45       *
  46       * @param stateupdates $updates the affected course elements track
  47       * @param stdClass $course the course object
  48       * @param int[] $ids the list of affected course module ids
  49       * @param int $targetsectionid optional target section id
  50       * @param int $targetcmid optional target cm id
  51       */
  52      public function cm_move(
  53          stateupdates $updates,
  54          stdClass $course,
  55          array $ids,
  56          ?int $targetsectionid = null,
  57          ?int $targetcmid = null
  58      ): void {
  59          // Validate target elements.
  60          if (!$targetsectionid && !$targetcmid) {
  61              throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid");
  62          }
  63  
  64          $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
  65          // The moveto_module function move elements before a specific target.
  66          // To keep the order the movements must be done in descending order (last activity first).
  67          $ids = $this->sort_cm_ids_by_course_position($course, $ids, true);
  68  
  69          // Target cm has more priority than target section.
  70          if (!empty($targetcmid)) {
  71              $this->validate_cms($course, [$targetcmid], __FUNCTION__);
  72              $targetcm = get_fast_modinfo($course)->get_cm($targetcmid);
  73              $targetsectionid = $targetcm->section;
  74          } else {
  75              $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
  76          }
  77  
  78          // The origin sections must be updated as well.
  79          $originalsections = [];
  80  
  81          $beforecmdid = $targetcmid;
  82          foreach ($ids as $cmid) {
  83              // An updated $modinfo is needed on every loop as activities list change.
  84              $modinfo = get_fast_modinfo($course);
  85              $cm = $modinfo->get_cm($cmid);
  86              $currentsectionid = $cm->section;
  87              $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
  88              $beforecm = (!empty($beforecmdid)) ? $modinfo->get_cm($beforecmdid) : null;
  89              if ($beforecm === null || $beforecm->id != $cmid) {
  90                  moveto_module($cm, $targetsection, $beforecm);
  91              }
  92              $beforecmdid = $cm->id;
  93              $updates->add_cm_put($cm->id);
  94              if ($currentsectionid != $targetsectionid) {
  95                  $originalsections[$currentsectionid] = true;
  96              }
  97              // If some of the original sections are also target sections, we don't need to update them.
  98              if (array_key_exists($targetsectionid, $originalsections)) {
  99                  unset($originalsections[$targetsectionid]);
 100              }
 101          }
 102  
 103          // Use section_state to return the full affected section and activities updated state.
 104          $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid);
 105  
 106          foreach (array_keys($originalsections) as $sectionid) {
 107              $updates->add_section_put($sectionid);
 108          }
 109      }
 110  
 111      /**
 112       * Sort the cm ids list depending on the course position.
 113       *
 114       * Some actions like move should be done in an specific order.
 115       *
 116       * @param stdClass $course the course object
 117       * @param int[] $cmids the array of section $ids
 118       * @param bool $descending if the sort order must be descending instead of ascending
 119       * @return int[] the array of section ids sorted by section number
 120       */
 121      protected function sort_cm_ids_by_course_position(
 122          stdClass $course,
 123          array $cmids,
 124          bool $descending = false
 125      ): array {
 126          $modinfo = get_fast_modinfo($course);
 127          $cmlist = array_keys($modinfo->get_cms());
 128          $cmposition = [];
 129          foreach ($cmids as $cmid) {
 130              $cmposition[$cmid] = array_search($cmid, $cmlist);
 131          }
 132          $sorting = ($descending) ? -1 : 1;
 133          $sortfunction = function ($acmid, $bcmid) use ($sorting, $cmposition) {
 134              return ($cmposition[$acmid] <=> $cmposition[$bcmid]) * $sorting;
 135          };
 136          usort($cmids, $sortfunction);
 137          return $cmids;
 138      }
 139  
 140      /**
 141       * Move course sections to another location in the same course.
 142       *
 143       * @param stateupdates $updates the affected course elements track
 144       * @param stdClass $course the course object
 145       * @param int[] $ids the list of affected course module ids
 146       * @param int $targetsectionid optional target section id
 147       * @param int $targetcmid optional target cm id
 148       */
 149      public function section_move(
 150          stateupdates $updates,
 151          stdClass $course,
 152          array $ids,
 153          ?int $targetsectionid = null,
 154          ?int $targetcmid = null
 155      ): void {
 156          // Validate target elements.
 157          if (!$targetsectionid) {
 158              throw new moodle_exception("Action cm_move requires targetsectionid");
 159          }
 160  
 161          $this->validate_sections($course, $ids, __FUNCTION__);
 162  
 163          $coursecontext = context_course::instance($course->id);
 164          require_capability('moodle/course:movesections', $coursecontext);
 165  
 166          $modinfo = get_fast_modinfo($course);
 167  
 168          // Target section.
 169          $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 170          $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
 171  
 172          $affectedsections = [$targetsection->section => true];
 173  
 174          $sections = $this->get_section_info($modinfo, $ids);
 175          foreach ($sections as $section) {
 176              $affectedsections[$section->section] = true;
 177              move_section_to($course, $section->section, $targetsection->section);
 178          }
 179  
 180          // Use section_state to return the section and activities updated state.
 181          $this->section_state($updates, $course, $ids, $targetsectionid);
 182  
 183          // All course sections can be renamed because of the resort.
 184          $allsections = $modinfo->get_section_info_all();
 185          foreach ($allsections as $section) {
 186              // Ignore the affected sections because they are already in the updates.
 187              if (isset($affectedsections[$section->section])) {
 188                  continue;
 189              }
 190              $updates->add_section_put($section->id);
 191          }
 192          // The section order is at a course level.
 193          $updates->add_course_put();
 194      }
 195  
 196      /**
 197       * Move course sections after to another location in the same course.
 198       *
 199       * @param stateupdates $updates the affected course elements track
 200       * @param stdClass $course the course object
 201       * @param int[] $ids the list of affected course module ids
 202       * @param int $targetsectionid optional target section id
 203       * @param int $targetcmid optional target cm id
 204       */
 205      public function section_move_after(
 206          stateupdates $updates,
 207          stdClass $course,
 208          array $ids,
 209          ?int $targetsectionid = null,
 210          ?int $targetcmid = null
 211      ): void {
 212          // Validate target elements.
 213          if (!$targetsectionid) {
 214              throw new moodle_exception("Action section_move_after requires targetsectionid");
 215          }
 216  
 217          $this->validate_sections($course, $ids, __FUNCTION__);
 218  
 219          $coursecontext = context_course::instance($course->id);
 220          require_capability('moodle/course:movesections', $coursecontext);
 221  
 222          // Section will move after the target section. This means it should be processed in
 223          // descending order to keep the relative course order.
 224          $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 225          $ids = $this->sort_section_ids_by_section_number($course, $ids, true);
 226  
 227          $format = course_get_format($course->id);
 228          $affectedsections = [$targetsectionid => true];
 229  
 230          foreach ($ids as $id) {
 231              // An update section_info is needed as section numbers can change on every section movement.
 232              $modinfo = get_fast_modinfo($course);
 233              $section = $modinfo->get_section_info_by_id($id, MUST_EXIST);
 234              $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
 235              $affectedsections[$section->id] = true;
 236              $format->move_section_after($section, $targetsection);
 237          }
 238  
 239          // Use section_state to return the section and activities updated state.
 240          $this->section_state($updates, $course, $ids, $targetsectionid);
 241  
 242          // All course sections can be renamed because of the resort.
 243          $modinfo = get_fast_modinfo($course);
 244          $allsections = $modinfo->get_section_info_all();
 245          foreach ($allsections as $section) {
 246              // Ignore the affected sections because they are already in the updates.
 247              if (isset($affectedsections[$section->id])) {
 248                  continue;
 249              }
 250              $updates->add_section_put($section->id);
 251          }
 252          // The section order is at a course level.
 253          $updates->add_course_put();
 254      }
 255  
 256      /**
 257       * Sort the sections ids depending on the section number.
 258       *
 259       * Some actions like move should be done in an specific order.
 260       *
 261       * @param stdClass $course the course object
 262       * @param int[] $sectionids the array of section $ids
 263       * @param bool $descending if the sort order must be descending instead of ascending
 264       * @return int[] the array of section ids sorted by section number
 265       */
 266      protected function sort_section_ids_by_section_number(
 267          stdClass $course,
 268          array $sectionids,
 269          bool $descending = false
 270      ): array {
 271          $sorting = ($descending) ? -1 : 1;
 272          $sortfunction = function ($asection, $bsection) use ($sorting) {
 273              return ($asection->section <=> $bsection->section) * $sorting;
 274          };
 275          $modinfo = get_fast_modinfo($course);
 276          $sections = $this->get_section_info($modinfo, $sectionids);
 277          uasort($sections, $sortfunction);
 278          return array_keys($sections);
 279      }
 280  
 281      /**
 282       * Create a course section.
 283       *
 284       * This method follows the same logic as changenumsections.php.
 285       *
 286       * @param stateupdates $updates the affected course elements track
 287       * @param stdClass $course the course object
 288       * @param int[] $ids not used
 289       * @param int $targetsectionid optional target section id (if not passed section will be appended)
 290       * @param int $targetcmid not used
 291       */
 292      public function section_add(
 293          stateupdates $updates,
 294          stdClass $course,
 295          array $ids = [],
 296          ?int $targetsectionid = null,
 297          ?int $targetcmid = null
 298      ): void {
 299  
 300          $coursecontext = context_course::instance($course->id);
 301          require_capability('moodle/course:update', $coursecontext);
 302  
 303          // Get course format settings.
 304          $format = course_get_format($course->id);
 305          $lastsectionnumber = $format->get_last_section_number();
 306          $maxsections = $format->get_max_sections();
 307  
 308          if ($lastsectionnumber >= $maxsections) {
 309              throw new moodle_exception('maxsectionslimit', 'moodle', $maxsections);
 310          }
 311  
 312          $modinfo = get_fast_modinfo($course);
 313  
 314          // Get target section.
 315          if ($targetsectionid) {
 316              $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 317              $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
 318              // Inserting sections at any position except in the very end requires capability to move sections.
 319              require_capability('moodle/course:movesections', $coursecontext);
 320              $insertposition = $targetsection->section + 1;
 321          } else {
 322              // Get last section.
 323              $insertposition = 0;
 324          }
 325  
 326          course_create_section($course, $insertposition);
 327  
 328          // Adding a section affects the full course structure.
 329          $this->course_state($updates, $course);
 330      }
 331  
 332      /**
 333       * Delete course sections.
 334       *
 335       * This method follows the same logic as editsection.php.
 336       *
 337       * @param stateupdates $updates the affected course elements track
 338       * @param stdClass $course the course object
 339       * @param int[] $ids section ids
 340       * @param int $targetsectionid not used
 341       * @param int $targetcmid not used
 342       */
 343      public function section_delete(
 344          stateupdates $updates,
 345          stdClass $course,
 346          array $ids = [],
 347          ?int $targetsectionid = null,
 348          ?int $targetcmid = null
 349      ): void {
 350  
 351          $coursecontext = context_course::instance($course->id);
 352          require_capability('moodle/course:update', $coursecontext);
 353          require_capability('moodle/course:movesections', $coursecontext);
 354  
 355          foreach ($ids as $sectionid) {
 356              // We need to get the latest modinfo on each iteration because the section numbers change.
 357              $modinfo = get_fast_modinfo($course);
 358              $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
 359              // Send all activity deletions.
 360              if (!empty($modinfo->sections[$section->section])) {
 361                  foreach ($modinfo->sections[$section->section] as $modnumber) {
 362                      $cm = $modinfo->cms[$modnumber];
 363                      $updates->add_cm_remove($cm->id);
 364                  }
 365              }
 366              course_delete_section($course, $section, true, true);
 367              $updates->add_section_remove($sectionid);
 368          }
 369  
 370          // Removing a section affects the full course structure.
 371          $this->course_state($updates, $course);
 372      }
 373  
 374      /**
 375       * Hide course sections.
 376       *
 377       * @param stateupdates $updates the affected course elements track
 378       * @param stdClass $course the course object
 379       * @param int[] $ids section ids
 380       * @param int $targetsectionid not used
 381       * @param int $targetcmid not used
 382       */
 383      public function section_hide(
 384          stateupdates $updates,
 385          stdClass $course,
 386          array $ids = [],
 387          ?int $targetsectionid = null,
 388          ?int $targetcmid = null
 389      ): void {
 390          $this->set_section_visibility($updates, $course, $ids, 0);
 391      }
 392  
 393      /**
 394       * Show course sections.
 395       *
 396       * @param stateupdates $updates the affected course elements track
 397       * @param stdClass $course the course object
 398       * @param int[] $ids section ids
 399       * @param int $targetsectionid not used
 400       * @param int $targetcmid not used
 401       */
 402      public function section_show(
 403          stateupdates $updates,
 404          stdClass $course,
 405          array $ids = [],
 406          ?int $targetsectionid = null,
 407          ?int $targetcmid = null
 408      ): void {
 409          $this->set_section_visibility($updates, $course, $ids, 1);
 410      }
 411  
 412      /**
 413       * Show course sections.
 414       *
 415       * @param stateupdates $updates the affected course elements track
 416       * @param stdClass $course the course object
 417       * @param int[] $ids section ids
 418       * @param int $visible the new visible value
 419       */
 420      protected function set_section_visibility (
 421          stateupdates $updates,
 422          stdClass $course,
 423          array $ids,
 424          int $visible
 425      ) {
 426          $this->validate_sections($course, $ids, __FUNCTION__);
 427          $coursecontext = context_course::instance($course->id);
 428          require_all_capabilities(['moodle/course:update', 'moodle/course:sectionvisibility'], $coursecontext);
 429  
 430          $modinfo = get_fast_modinfo($course);
 431  
 432          foreach ($ids as $sectionid) {
 433              $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
 434              course_update_section($course, $section, ['visible' => $visible]);
 435          }
 436          $this->section_state($updates, $course, $ids);
 437      }
 438  
 439      /**
 440       * Show course cms.
 441       *
 442       * @param stateupdates $updates the affected course elements track
 443       * @param stdClass $course the course object
 444       * @param int[] $ids cm ids
 445       * @param int $targetsectionid not used
 446       * @param int $targetcmid not used
 447       */
 448      public function cm_show(
 449          stateupdates $updates,
 450          stdClass $course,
 451          array $ids = [],
 452          ?int $targetsectionid = null,
 453          ?int $targetcmid = null
 454      ): void {
 455          $this->set_cm_visibility($updates, $course, $ids, 1, 1);
 456      }
 457  
 458      /**
 459       * Hide course cms.
 460       *
 461       * @param stateupdates $updates the affected course elements track
 462       * @param stdClass $course the course object
 463       * @param int[] $ids cm ids
 464       * @param int $targetsectionid not used
 465       * @param int $targetcmid not used
 466       */
 467      public function cm_hide(
 468          stateupdates $updates,
 469          stdClass $course,
 470          array $ids = [],
 471          ?int $targetsectionid = null,
 472          ?int $targetcmid = null
 473      ): void {
 474          $this->set_cm_visibility($updates, $course, $ids, 0, 1);
 475      }
 476  
 477      /**
 478       * Stealth course cms.
 479       *
 480       * @param stateupdates $updates the affected course elements track
 481       * @param stdClass $course the course object
 482       * @param int[] $ids cm ids
 483       * @param int $targetsectionid not used
 484       * @param int $targetcmid not used
 485       */
 486      public function cm_stealth(
 487          stateupdates $updates,
 488          stdClass $course,
 489          array $ids = [],
 490          ?int $targetsectionid = null,
 491          ?int $targetcmid = null
 492      ): void {
 493          $this->set_cm_visibility($updates, $course, $ids, 1, 0);
 494      }
 495  
 496      /**
 497       * Internal method to define the cm visibility.
 498       *
 499       * @param stateupdates $updates the affected course elements track
 500       * @param stdClass $course the course object
 501       * @param int[] $ids cm ids
 502       * @param int $visible the new visible value
 503       * @param int $coursevisible the new course visible value
 504       */
 505      protected function set_cm_visibility(
 506          stateupdates $updates,
 507          stdClass $course,
 508          array $ids,
 509          int $visible,
 510          int $coursevisible
 511      ): void {
 512          global $CFG;
 513  
 514          $this->validate_cms(
 515              $course,
 516              $ids,
 517              __FUNCTION__,
 518              ['moodle/course:manageactivities', 'moodle/course:activityvisibility']
 519          );
 520  
 521          $format = course_get_format($course->id);
 522          $modinfo = get_fast_modinfo($course);
 523  
 524          $cms = $this->get_cm_info($modinfo, $ids);
 525          foreach ($cms as $cm) {
 526              // Check stealth availability.
 527              if (!$coursevisible) {
 528                  $section = $cm->get_section_info();
 529                  $allowstealth = !empty($CFG->allowstealth) && $format->allow_stealth_module_visibility($cm, $section);
 530                  $coursevisible = ($allowstealth) ? 0 : 1;
 531              }
 532              set_coursemodule_visible($cm->id, $visible, $coursevisible, false);
 533              $modcontext = context_module::instance($cm->id);
 534              course_module_updated::create_from_cm($cm, $modcontext)->trigger();
 535          }
 536          course_modinfo::purge_course_modules_cache($course->id, $ids);
 537          rebuild_course_cache($course->id, false, true);
 538  
 539          foreach ($cms as $cm) {
 540              $updates->add_cm_put($cm->id);
 541          }
 542      }
 543  
 544      /**
 545       * Duplicate a course modules instances into the same course.
 546       *
 547       * @param stateupdates $updates the affected course elements track
 548       * @param stdClass $course the course object
 549       * @param int[] $ids course modules ids to duplicate
 550       * @param int|null $targetsectionid optional target section id destination
 551       * @param int|null $targetcmid optional target before cm id destination
 552       */
 553      public function cm_duplicate(
 554          stateupdates $updates,
 555          stdClass $course,
 556          array $ids = [],
 557          ?int $targetsectionid = null,
 558          ?int $targetcmid = null
 559      ): void {
 560          $this->validate_cms(
 561              $course,
 562              $ids,
 563              __FUNCTION__,
 564              ['moodle/course:manageactivities', 'moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport']
 565          );
 566  
 567          $modinfo = get_fast_modinfo($course);
 568          $cms = $this->get_cm_info($modinfo, $ids);
 569  
 570          // Check capabilities on every activity context.
 571          foreach ($cms as $cm) {
 572              if (!course_allowed_module($course, $cm->modname)) {
 573                  throw new moodle_exception('No permission to create that activity');
 574              }
 575          }
 576  
 577          $targetsection = null;
 578          if (!empty($targetsectionid)) {
 579              $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 580              $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
 581          }
 582  
 583          $beforecm = null;
 584          if (!empty($targetcmid)) {
 585              $this->validate_cms($course, [$targetcmid], __FUNCTION__);
 586              $beforecm = $modinfo->get_cm($targetcmid);
 587              $targetsection = $modinfo->get_section_info_by_id($beforecm->section, MUST_EXIST);
 588          }
 589  
 590          // Duplicate course modules.
 591          $affectedcmids = [];
 592          foreach ($cms as $cm) {
 593              if ($newcm = duplicate_module($course, $cm)) {
 594                  if ($targetsection) {
 595                      moveto_module($newcm, $targetsection, $beforecm);
 596                  } else {
 597                      $affectedcmids[] = $newcm->id;
 598                  }
 599              }
 600          }
 601  
 602          if ($targetsection) {
 603              $this->section_state($updates, $course, [$targetsection->id]);
 604          } else {
 605              $this->cm_state($updates, $course, $affectedcmids);
 606          }
 607      }
 608  
 609      /**
 610       * Delete course cms.
 611       *
 612       * @param stateupdates $updates the affected course elements track
 613       * @param stdClass $course the course object
 614       * @param int[] $ids section ids
 615       * @param int $targetsectionid not used
 616       * @param int $targetcmid not used
 617       */
 618      public function cm_delete(
 619          stateupdates $updates,
 620          stdClass $course,
 621          array $ids = [],
 622          ?int $targetsectionid = null,
 623          ?int $targetcmid = null
 624      ): void {
 625  
 626          $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
 627  
 628          $format = course_get_format($course->id);
 629          $modinfo = get_fast_modinfo($course);
 630          $affectedsections = [];
 631  
 632          $cms = $this->get_cm_info($modinfo, $ids);
 633          foreach ($cms as $cm) {
 634              $section = $cm->get_section_info();
 635              $affectedsections[$section->id] = $section;
 636              $format->delete_module($cm, true);
 637              $updates->add_cm_remove($cm->id);
 638          }
 639  
 640          foreach ($affectedsections as $sectionid => $section) {
 641              $updates->add_section_put($sectionid);
 642          }
 643      }
 644  
 645      /**
 646       * Move course cms to the right. Indent = 1.
 647       *
 648       * @param stateupdates $updates the affected course elements track
 649       * @param stdClass $course the course object
 650       * @param int[] $ids cm ids
 651       * @param int $targetsectionid not used
 652       * @param int $targetcmid not used
 653       */
 654      public function cm_moveright(
 655          stateupdates $updates,
 656          stdClass $course,
 657          array $ids = [],
 658          ?int $targetsectionid = null,
 659          ?int $targetcmid = null
 660      ): void {
 661          $this->set_cm_indentation($updates, $course, $ids, 1);
 662      }
 663  
 664      /**
 665       * Move course cms to the left. Indent = 0.
 666       *
 667       * @param stateupdates $updates the affected course elements track
 668       * @param stdClass $course the course object
 669       * @param int[] $ids cm ids
 670       * @param int $targetsectionid not used
 671       * @param int $targetcmid not used
 672       */
 673      public function cm_moveleft(
 674          stateupdates $updates,
 675          stdClass $course,
 676          array $ids = [],
 677          ?int $targetsectionid = null,
 678          ?int $targetcmid = null
 679      ): void {
 680          $this->set_cm_indentation($updates, $course, $ids, 0);
 681      }
 682  
 683      /**
 684       * Internal method to define the cm indentation level.
 685       *
 686       * @param stateupdates $updates the affected course elements track
 687       * @param stdClass $course the course object
 688       * @param int[] $ids cm ids
 689       * @param int $indent new value for indentation
 690       */
 691      protected function set_cm_indentation(
 692          stateupdates $updates,
 693          stdClass $course,
 694          array $ids,
 695          int $indent
 696      ): void {
 697          global $DB;
 698  
 699          $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
 700          $modinfo = get_fast_modinfo($course);
 701          $cms = $this->get_cm_info($modinfo, $ids);
 702          list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED);
 703          $DB->set_field_select('course_modules', 'indent', $indent, "id $insql", $inparams);
 704          rebuild_course_cache($course->id, false, true);
 705          foreach ($cms as $cm) {
 706              $modcontext = context_module::instance($cm->id);
 707              course_module_updated::create_from_cm($cm, $modcontext)->trigger();
 708              $updates->add_cm_put($cm->id);
 709          }
 710      }
 711  
 712      /**
 713       * Set NOGROUPS const value to cms groupmode.
 714       *
 715       * @param stateupdates $updates the affected course elements track
 716       * @param stdClass $course the course object
 717       * @param int[] $ids cm ids
 718       * @param int $targetsectionid not used
 719       * @param int $targetcmid not used
 720       */
 721      public function cm_nogroups(
 722          stateupdates $updates,
 723          stdClass $course,
 724          array $ids = [],
 725          ?int $targetsectionid = null,
 726          ?int $targetcmid = null
 727      ): void {
 728          $this->set_cm_groupmode($updates, $course, $ids, NOGROUPS);
 729      }
 730  
 731      /**
 732       * Set VISIBLEGROUPS const value to cms groupmode.
 733       *
 734       * @param stateupdates $updates the affected course elements track
 735       * @param stdClass $course the course object
 736       * @param int[] $ids cm ids
 737       * @param int $targetsectionid not used
 738       * @param int $targetcmid not used
 739       */
 740      public function cm_visiblegroups(
 741          stateupdates $updates,
 742          stdClass $course,
 743          array $ids = [],
 744          ?int $targetsectionid = null,
 745          ?int $targetcmid = null
 746      ): void {
 747          $this->set_cm_groupmode($updates, $course, $ids, VISIBLEGROUPS);
 748      }
 749  
 750      /**
 751       * Set SEPARATEGROUPS const value to cms groupmode.
 752       *
 753       * @param stateupdates $updates the affected course elements track
 754       * @param stdClass $course the course object
 755       * @param int[] $ids cm ids
 756       * @param int $targetsectionid not used
 757       * @param int $targetcmid not used
 758       */
 759      public function cm_separategroups(
 760          stateupdates $updates,
 761          stdClass $course,
 762          array $ids = [],
 763          ?int $targetsectionid = null,
 764          ?int $targetcmid = null
 765      ): void {
 766          $this->set_cm_groupmode($updates, $course, $ids, SEPARATEGROUPS);
 767      }
 768  
 769      /**
 770       * Internal method to define the cm groupmode value.
 771       *
 772       * @param stateupdates $updates the affected course elements track
 773       * @param stdClass $course the course object
 774       * @param int[] $ids cm ids
 775       * @param int $groupmode new value for groupmode: NOGROUPS, SEPARATEGROUPS, VISIBLEGROUPS
 776       */
 777      protected function set_cm_groupmode(
 778          stateupdates $updates,
 779          stdClass $course,
 780          array $ids,
 781          int $groupmode
 782      ): void {
 783          global $DB;
 784  
 785          $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
 786          $modinfo = get_fast_modinfo($course);
 787          $cms = $this->get_cm_info($modinfo, $ids);
 788          list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED);
 789          $DB->set_field_select('course_modules', 'groupmode', $groupmode, "id $insql", $inparams);
 790          rebuild_course_cache($course->id, false, true);
 791          foreach ($cms as $cm) {
 792              $modcontext = context_module::instance($cm->id);
 793              course_module_updated::create_from_cm($cm, $modcontext)->trigger();
 794              $updates->add_cm_put($cm->id);
 795          }
 796      }
 797  
 798      /**
 799       * Extract several cm_info from the course_modinfo.
 800       *
 801       * @param course_modinfo $modinfo the course modinfo.
 802       * @param int[] $ids the course modules $ids
 803       * @return cm_info[] the extracted cm_info objects
 804       */
 805      protected function get_cm_info (course_modinfo $modinfo, array $ids): array {
 806          $cms = [];
 807          foreach ($ids as $cmid) {
 808              $cms[$cmid] = $modinfo->get_cm($cmid);
 809          }
 810          return $cms;
 811      }
 812  
 813      /**
 814       * Extract several section_info from the course_modinfo.
 815       *
 816       * @param course_modinfo $modinfo the course modinfo.
 817       * @param int[] $ids the course modules $ids
 818       * @return section_info[] the extracted section_info objects
 819       */
 820      protected function get_section_info(course_modinfo $modinfo, array $ids): array {
 821          $sections = [];
 822          foreach ($ids as $sectionid) {
 823              $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid);
 824          }
 825          return $sections;
 826      }
 827  
 828      /**
 829       * Update the course content section collapsed value.
 830       *
 831       * @param stateupdates $updates the affected course elements track
 832       * @param stdClass $course the course object
 833       * @param int[] $ids the collapsed section ids
 834       * @param int $targetsectionid not used
 835       * @param int $targetcmid not used
 836       */
 837      public function section_content_collapsed(
 838          stateupdates $updates,
 839          stdClass $course,
 840          array $ids = [],
 841          ?int $targetsectionid = null,
 842          ?int $targetcmid = null
 843      ): void {
 844          if (!empty($ids)) {
 845              $this->validate_sections($course, $ids, __FUNCTION__);
 846          }
 847          $format = course_get_format($course->id);
 848          $format->set_sections_preference('contentcollapsed', $ids);
 849      }
 850  
 851      /**
 852       * Update the course index section collapsed value.
 853       *
 854       * @param stateupdates $updates the affected course elements track
 855       * @param stdClass $course the course object
 856       * @param int[] $ids the collapsed section ids
 857       * @param int $targetsectionid not used
 858       * @param int $targetcmid not used
 859       */
 860      public function section_index_collapsed(
 861          stateupdates $updates,
 862          stdClass $course,
 863          array $ids = [],
 864          ?int $targetsectionid = null,
 865          ?int $targetcmid = null
 866      ): void {
 867          if (!empty($ids)) {
 868              $this->validate_sections($course, $ids, __FUNCTION__);
 869          }
 870          $format = course_get_format($course->id);
 871          $format->set_sections_preference('indexcollapsed', $ids);
 872      }
 873  
 874      /**
 875       * Add the update messages of the updated version of any cm and section related to the cm ids.
 876       *
 877       * This action is mainly used by legacy actions to partially update the course state when the
 878       * result of core_course_edit_module is not enough to generate the correct state data.
 879       *
 880       * @param stateupdates $updates the affected course elements track
 881       * @param stdClass $course the course object
 882       * @param int[] $ids the list of affected course module ids
 883       * @param int $targetsectionid optional target section id
 884       * @param int $targetcmid optional target cm id
 885       */
 886      public function cm_state(
 887          stateupdates $updates,
 888          stdClass $course,
 889          array $ids,
 890          ?int $targetsectionid = null,
 891          ?int $targetcmid = null
 892      ): void {
 893  
 894          // Collect all section and cm to return.
 895          $cmids = [];
 896          foreach ($ids as $cmid) {
 897              $cmids[$cmid] = true;
 898          }
 899          if ($targetcmid) {
 900              $cmids[$targetcmid] = true;
 901          }
 902  
 903          $sectionids = [];
 904          if ($targetsectionid) {
 905              $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
 906              $sectionids[$targetsectionid] = true;
 907          }
 908  
 909          $this->validate_cms($course, array_keys($cmids), __FUNCTION__);
 910  
 911          $modinfo = course_modinfo::instance($course);
 912  
 913          foreach (array_keys($cmids) as $cmid) {
 914  
 915              // Add this action to updates array.
 916              $updates->add_cm_put($cmid);
 917  
 918              $cm = $modinfo->get_cm($cmid);
 919              $sectionids[$cm->section] = true;
 920          }
 921  
 922          foreach (array_keys($sectionids) as $sectionid) {
 923              $updates->add_section_put($sectionid);
 924          }
 925      }
 926  
 927      /**
 928       * Add the update messages of the updated version of any cm and section related to the section ids.
 929       *
 930       * This action is mainly used by legacy actions to partially update the course state when the
 931       * result of core_course_edit_module is not enough to generate the correct state data.
 932       *
 933       * @param stateupdates $updates the affected course elements track
 934       * @param stdClass $course the course object
 935       * @param int[] $ids the list of affected course section ids
 936       * @param int $targetsectionid optional target section id
 937       * @param int $targetcmid optional target cm id
 938       */
 939      public function section_state(
 940          stateupdates $updates,
 941          stdClass $course,
 942          array $ids,
 943          ?int $targetsectionid = null,
 944          ?int $targetcmid = null
 945      ): void {
 946  
 947          $cmids = [];
 948          if ($targetcmid) {
 949              $this->validate_cms($course, [$targetcmid], __FUNCTION__);
 950              $cmids[$targetcmid] = true;
 951          }
 952  
 953          $sectionids = [];
 954          foreach ($ids as $sectionid) {
 955              $sectionids[$sectionid] = true;
 956          }
 957          if ($targetsectionid) {
 958              $sectionids[$targetsectionid] = true;
 959          }
 960  
 961          $this->validate_sections($course, array_keys($sectionids), __FUNCTION__);
 962  
 963          $modinfo = course_modinfo::instance($course);
 964  
 965          foreach (array_keys($sectionids) as $sectionid) {
 966              $sectioninfo = $modinfo->get_section_info_by_id($sectionid);
 967              $updates->add_section_put($sectionid);
 968              // Add cms.
 969              if (empty($modinfo->sections[$sectioninfo->section])) {
 970                  continue;
 971              }
 972  
 973              foreach ($modinfo->sections[$sectioninfo->section] as $modnumber) {
 974                  $mod = $modinfo->cms[$modnumber];
 975                  if ($mod->is_visible_on_course_page()) {
 976                      $cmids[$mod->id] = true;
 977                  }
 978              }
 979          }
 980  
 981          foreach (array_keys($cmids) as $cmid) {
 982              // Add this action to updates array.
 983              $updates->add_cm_put($cmid);
 984          }
 985      }
 986  
 987      /**
 988       * Add all the update messages from the complete course state.
 989       *
 990       * This action is mainly used by legacy actions to partially update the course state when the
 991       * result of core_course_edit_module is not enough to generate the correct state data.
 992       *
 993       * @param stateupdates $updates the affected course elements track
 994       * @param stdClass $course the course object
 995       * @param int[] $ids the list of affected course module ids (not used)
 996       * @param int $targetsectionid optional target section id (not used)
 997       * @param int $targetcmid optional target cm id (not used)
 998       */
 999      public function course_state(
1000          stateupdates $updates,
1001          stdClass $course,
1002          array $ids = [],
1003          ?int $targetsectionid = null,
1004          ?int $targetcmid = null
1005      ): void {
1006  
1007          $modinfo = course_modinfo::instance($course);
1008  
1009          $updates->add_course_put();
1010  
1011          // Add sections updates.
1012          $sections = $modinfo->get_section_info_all();
1013          $sectionids = [];
1014          foreach ($sections as $sectioninfo) {
1015              $sectionids[] = $sectioninfo->id;
1016          }
1017          if (!empty($sectionids)) {
1018              $this->section_state($updates, $course, $sectionids);
1019          }
1020      }
1021  
1022      /**
1023       * Checks related to sections: course format support them, all given sections exist and topic 0 is not included.
1024       *
1025       * @param stdClass $course The course where given $sectionids belong.
1026       * @param array $sectionids List of sections to validate.
1027       * @param string|null $info additional information in case of error (default null).
1028       * @throws moodle_exception if any id is not valid
1029       */
1030      protected function validate_sections(stdClass $course, array $sectionids, ?string $info = null): void {
1031          global $DB;
1032  
1033          if (empty($sectionids)) {
1034              throw new moodle_exception('emptysectionids', 'core', null, $info);
1035          }
1036  
1037          // No section actions are allowed if course format does not support sections.
1038          $courseformat = course_get_format($course->id);
1039          if (!$courseformat->uses_sections()) {
1040              throw new moodle_exception('sectionactionnotsupported', 'core', null, $info);
1041          }
1042  
1043          list($insql, $inparams) = $DB->get_in_or_equal($sectionids, SQL_PARAMS_NAMED);
1044  
1045          // Check if all the given sections exist.
1046          $couintsections = $DB->count_records_select('course_sections', "id $insql", $inparams);
1047          if ($couintsections != count($sectionids)) {
1048              throw new moodle_exception('unexistingsectionid', 'core', null, $info);
1049          }
1050      }
1051  
1052      /**
1053       * Checks related to course modules: all given cm exist and the user has the required capabilities.
1054       *
1055       * @param stdClass $course The course where given $cmids belong.
1056       * @param array $cmids List of course module ids to validate.
1057       * @param string $info additional information in case of error.
1058       * @param array $capabilities optional capabilities checks per each cm context.
1059       * @throws moodle_exception if any id is not valid
1060       */
1061      protected function validate_cms(stdClass $course, array $cmids, ?string $info = null, array $capabilities = []): void {
1062  
1063          if (empty($cmids)) {
1064              throw new moodle_exception('emptycmids', 'core', null, $info);
1065          }
1066  
1067          $moduleinfo = get_fast_modinfo($course->id);
1068          $intersect = array_intersect($cmids, array_keys($moduleinfo->get_cms()));
1069          if (count($cmids) != count($intersect)) {
1070              throw new moodle_exception('unexistingcmid', 'core', null, $info);
1071          }
1072          if (!empty($capabilities)) {
1073              foreach ($cmids as $cmid) {
1074                  $modcontext = context_module::instance($cmid);
1075                  require_all_capabilities($capabilities, $modcontext);
1076              }
1077          }
1078      }
1079  }