Search moodle.org's
Developer Documentation

See Release Notes

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

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