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 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Course and category management helper class.
  19   *
  20   * @package    core_course
  21   * @copyright  2013 Sam Hemelryk
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_course\management;
  26  
  27  defined('MOODLE_INTERNAL') || die;
  28  
  29  require_once($CFG->dirroot . '/course/lib.php');
  30  
  31  /**
  32   * Course and category management interface helper class.
  33   *
  34   * This class provides methods useful to the course and category management interfaces.
  35   * Many of the methods on this class are static and serve one of two purposes.
  36   *  1.  encapsulate functionality in an effort to ensure minimal changes between the different
  37   *      methods of interaction. Specifically browser, AJAX and webservice.
  38   *  2.  abstract logic for acquiring actions away from output so that renderers may use them without
  39   *      having to include any logic or capability checks.
  40   *
  41   * @package    core_course
  42   * @copyright  2013 Sam Hemelryk
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class helper {
  46  
  47      /**
  48       * The expanded category structure if its already being loaded from the cache.
  49       * @var null|array
  50       */
  51      protected static $expandedcategories = null;
  52  
  53      /**
  54       * Returns course details in an array ready to be printed.
  55       *
  56       * @global \moodle_database $DB
  57       * @param \core_course_list_element $course
  58       * @return array
  59       */
  60      public static function get_course_detail_array(\core_course_list_element $course) {
  61          global $DB;
  62  
  63          $canaccess = $course->can_access();
  64  
  65          $format = \course_get_format($course->id);
  66          $modinfo = \get_fast_modinfo($course->id);
  67          $modules = $modinfo->get_used_module_names();
  68          $sections = array();
  69          if ($format->uses_sections()) {
  70              foreach ($modinfo->get_section_info_all() as $section) {
  71                  if ($section->uservisible) {
  72                      $sections[] = $format->get_section_name($section);
  73                  }
  74              }
  75          }
  76  
  77          $category = \core_course_category::get($course->category);
  78          $categoryurl = new \moodle_url('/course/management.php', array('categoryid' => $course->category));
  79          $categoryname = $category->get_formatted_name();
  80  
  81          $details = array(
  82              'fullname' => array(
  83                  'key' => \get_string('fullname'),
  84                  'value' => $course->get_formatted_fullname()
  85              ),
  86              'shortname' => array(
  87                  'key' => \get_string('shortname'),
  88                  'value' => $course->get_formatted_shortname()
  89              ),
  90              'idnumber' => array(
  91                  'key' => \get_string('idnumber'),
  92                  'value' => s($course->idnumber)
  93              ),
  94              'category' => array(
  95                  'key' => \get_string('category'),
  96                  'value' => \html_writer::link($categoryurl, $categoryname)
  97              )
  98          );
  99          if (has_capability('moodle/site:accessallgroups', $course->get_context())) {
 100              $groups = \groups_get_course_data($course->id);
 101              $details += array(
 102                  'groupings' => array(
 103                      'key' => \get_string('groupings', 'group'),
 104                      'value' => count($groups->groupings)
 105                  ),
 106                  'groups' => array(
 107                      'key' => \get_string('groups'),
 108                      'value' => count($groups->groups)
 109                  )
 110              );
 111          }
 112          if ($canaccess) {
 113              $names = \role_get_names($course->get_context());
 114              $sql = 'SELECT ra.roleid, COUNT(ra.id) AS rolecount
 115                        FROM {role_assignments} ra
 116                       WHERE ra.contextid = :contextid
 117                    GROUP BY ra.roleid';
 118              $rolecounts = $DB->get_records_sql($sql, array('contextid' => $course->get_context()->id));
 119              $roledetails = array();
 120              foreach ($rolecounts as $result) {
 121                  $a = new \stdClass;
 122                  $a->role = $names[$result->roleid]->localname;
 123                  $a->count = $result->rolecount;
 124                  $roledetails[] = \get_string('assignedrolecount', 'moodle', $a);
 125              }
 126  
 127              $details['roleassignments'] = array(
 128                  'key' => \get_string('roleassignments'),
 129                  'value' => join('<br />', $roledetails)
 130              );
 131          }
 132          if ($course->can_review_enrolments()) {
 133              $enrolmentlines = array();
 134              $instances = \enrol_get_instances($course->id, true);
 135              $plugins = \enrol_get_plugins(true);
 136              foreach ($instances as $instance) {
 137                  if (!isset($plugins[$instance->enrol])) {
 138                      // Weird.
 139                      continue;
 140                  }
 141                  $plugin = $plugins[$instance->enrol];
 142                  $enrolmentlines[] = $plugin->get_instance_name($instance);
 143              }
 144              $details['enrolmentmethods'] = array(
 145                  'key' => \get_string('enrolmentmethods'),
 146                  'value' => join('<br />', $enrolmentlines)
 147              );
 148          }
 149          if ($canaccess) {
 150              $details['format'] = array(
 151                  'key' => \get_string('format'),
 152                  'value' => \course_get_format($course)->get_format_name()
 153              );
 154              $details['sections'] = array(
 155                  'key' => \get_string('sections'),
 156                  'value' => join('<br />', $sections)
 157              );
 158              $details['modulesused'] = array(
 159                  'key' => \get_string('modulesused'),
 160                  'value' =>  join('<br />', $modules)
 161              );
 162          }
 163          return $details;
 164      }
 165  
 166      /**
 167       * Returns an array of actions that can be performed upon a category being shown in a list.
 168       *
 169       * @param \core_course_category $category
 170       * @return array
 171       */
 172      public static function get_category_listitem_actions(\core_course_category $category) {
 173          global $CFG;
 174  
 175          $manageurl = new \moodle_url('/course/management.php', array('categoryid' => $category->id));
 176          $baseurl = new \moodle_url($manageurl, array('sesskey' => \sesskey()));
 177          $actions = array();
 178  
 179          // View link.
 180          $actions['view'] = [
 181              'url' => new \moodle_url('/course/index.php', ['categoryid' => $category->id]),
 182              'icon' => null,
 183              'string' => get_string('view')
 184          ];
 185  
 186          // Edit.
 187          if ($category->can_edit()) {
 188              $actions['edit'] = array(
 189                  'url' => new \moodle_url('/course/editcategory.php', array('id' => $category->id)),
 190                  'icon' => new \pix_icon('t/edit', new \lang_string('edit')),
 191                  'string' => new \lang_string('edit')
 192              );
 193          }
 194  
 195          // Show/Hide.
 196          if ($category->can_change_visibility()) {
 197              // We always show both icons and then just toggle the display of the invalid option with CSS.
 198              $actions['hide'] = array(
 199                  'url' => new \moodle_url($baseurl, array('action' => 'hidecategory')),
 200                  'icon' => new \pix_icon('t/hide', new \lang_string('hide')),
 201                  'string' => new \lang_string('hide')
 202              );
 203              $actions['show'] = array(
 204                  'url' => new \moodle_url($baseurl, array('action' => 'showcategory')),
 205                  'icon' => new \pix_icon('t/show', new \lang_string('show')),
 206                  'string' => new \lang_string('show')
 207              );
 208          }
 209  
 210          // Move up/down.
 211          if ($category->can_change_sortorder()) {
 212              $actions['moveup'] = array(
 213                  'url' => new \moodle_url($baseurl, array('action' => 'movecategoryup')),
 214                  'icon' => new \pix_icon('t/up', new \lang_string('moveup')),
 215                  'string' => new \lang_string('moveup')
 216              );
 217              $actions['movedown'] = array(
 218                  'url' => new \moodle_url($baseurl, array('action' => 'movecategorydown')),
 219                  'icon' => new \pix_icon('t/down', new \lang_string('movedown')),
 220                  'string' => new \lang_string('movedown')
 221              );
 222          }
 223  
 224          if ($category->can_create_subcategory()) {
 225              $actions['createnewsubcategory'] = array(
 226                  'url' => new \moodle_url('/course/editcategory.php', array('parent' => $category->id)),
 227                  'icon' => new \pix_icon('i/withsubcat', new \lang_string('createnewsubcategory')),
 228                  'string' => new \lang_string('createnewsubcategory')
 229              );
 230          }
 231  
 232          // Resort.
 233          if ($category->can_resort_subcategories() && $category->has_children()) {
 234              $actions['resortbyname'] = array(
 235                  'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'name')),
 236                  'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
 237                  'string' => new \lang_string('resortsubcategoriesby', 'moodle' , get_string('categoryname'))
 238              );
 239              $actions['resortbynamedesc'] = array(
 240                  'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'namedesc')),
 241                  'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
 242                  'string' => new \lang_string('resortsubcategoriesbyreverse', 'moodle', get_string('categoryname'))
 243              );
 244              $actions['resortbyidnumber'] = array(
 245                  'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'idnumber')),
 246                  'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
 247                  'string' => new \lang_string('resortsubcategoriesby', 'moodle', get_string('idnumbercoursecategory'))
 248              );
 249              $actions['resortbyidnumberdesc'] = array(
 250                  'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'idnumberdesc')),
 251                  'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
 252                  'string' => new \lang_string('resortsubcategoriesbyreverse', 'moodle', get_string('idnumbercoursecategory'))
 253              );
 254          }
 255  
 256          // Delete.
 257          if (!empty($category->move_content_targets_list()) || $category->can_delete_full()) {
 258              $actions['delete'] = array(
 259                  'url' => new \moodle_url($baseurl, array('action' => 'deletecategory')),
 260                  'icon' => new \pix_icon('t/delete', new \lang_string('delete')),
 261                  'string' => new \lang_string('delete')
 262              );
 263          }
 264  
 265          // Permissions.
 266          if ($category->can_review_permissions()) {
 267              $actions['permissions'] = array(
 268                  'url' => new \moodle_url('/admin/roles/permissions.php', ['contextid' => $category->get_context()->id]),
 269                  'icon' => new \pix_icon('i/permissions', new \lang_string('permissions', 'role')),
 270                  'string' => new \lang_string('permissions', 'role')
 271              );
 272          }
 273  
 274          // Context locking.
 275          if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $category->get_context())) {
 276              $parentcontext = $category->get_context()->get_parent_context();
 277              if (empty($parentcontext) || !$parentcontext->locked) {
 278                  if ($category->get_context()->locked) {
 279                      $lockicon = 'i/unlock';
 280                      $lockstring = get_string('managecontextunlock', 'admin');
 281                  } else {
 282                      $lockicon = 'i/lock';
 283                      $lockstring = get_string('managecontextlock', 'admin');
 284                  }
 285                  $actions['managecontextlock'] = [
 286                      'url' => new \moodle_url('/admin/lock.php', [
 287                              'id' => $category->get_context()->id,
 288                              'returnurl' => $manageurl->out_as_local_url(false),
 289                          ]),
 290                      'icon' => new \pix_icon($lockicon, $lockstring),
 291                      'string' => $lockstring,
 292                  ];
 293              }
 294          }
 295  
 296          // Cohorts.
 297          if ($category->can_review_cohorts()) {
 298              $actions['cohorts'] = array(
 299                  'url' => new \moodle_url('/cohort/index.php', array('contextid' => $category->get_context()->id)),
 300                  'icon' => new \pix_icon('t/cohort', new \lang_string('cohorts', 'cohort')),
 301                  'string' => new \lang_string('cohorts', 'cohort')
 302              );
 303          }
 304  
 305          // Filters.
 306          if ($category->can_review_filters()) {
 307              $actions['filters'] = array(
 308                  'url' => new \moodle_url('/filter/manage.php', array('contextid' => $category->get_context()->id,
 309                      'return' => 'management')),
 310                  'icon' => new \pix_icon('i/filter', new \lang_string('filters', 'admin')),
 311                  'string' => new \lang_string('filters', 'admin')
 312              );
 313          }
 314  
 315          if ($category->can_restore_courses_into()) {
 316              $actions['restore'] = array(
 317                  'url' => new \moodle_url('/backup/restorefile.php', array('contextid' => $category->get_context()->id)),
 318                  'icon' => new \pix_icon('i/restore', new \lang_string('restorecourse', 'admin')),
 319                  'string' => new \lang_string('restorecourse', 'admin')
 320              );
 321          }
 322          // Recyclebyn.
 323          if (\tool_recyclebin\category_bin::is_enabled()) {
 324              $categorybin = new \tool_recyclebin\category_bin($category->id);
 325              if ($categorybin->can_view()) {
 326                  $autohide = get_config('tool_recyclebin', 'autohide');
 327                  if ($autohide) {
 328                      $items = $categorybin->get_items();
 329                  } else {
 330                      $items = [];
 331                  }
 332                  if (!$autohide || !empty($items)) {
 333                      $pluginname = get_string('pluginname', 'tool_recyclebin');
 334                      $actions['recyclebin'] = [
 335                         'url' => new \moodle_url('/admin/tool/recyclebin/index.php', ['contextid' => $category->get_context()->id]),
 336                         'icon' => new \pix_icon('trash', $pluginname, 'tool_recyclebin'),
 337                         'string' => $pluginname
 338                      ];
 339                  }
 340              }
 341          }
 342  
 343          // Content bank.
 344          if ($category->has_contentbank()) {
 345              $url = new \moodle_url('/contentbank/index.php', ['contextid' => $category->get_context()->id]);
 346              $actions['contentbank'] = [
 347                  'url' => $url,
 348                  'icon' => new \pix_icon('i/contentbank', ''),
 349                  'string' => get_string('contentbank')
 350              ];
 351          }
 352  
 353          return $actions;
 354      }
 355  
 356      /**
 357       * Returns an array of actions for a course listitem.
 358       *
 359       * @param \core_course_category $category
 360       * @param \core_course_list_element $course
 361       * @return array
 362       */
 363      public static function get_course_listitem_actions(\core_course_category $category, \core_course_list_element $course) {
 364          $baseurl = new \moodle_url(
 365              '/course/management.php',
 366              array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => \sesskey())
 367          );
 368          $actions = array();
 369          // Edit.
 370          if ($course->can_edit()) {
 371              $actions[] = array(
 372                  'url' => new \moodle_url('/course/edit.php', array('id' => $course->id, 'returnto' => 'catmanage')),
 373                  'icon' => new \pix_icon('t/edit', \get_string('edit')),
 374                  'attributes' => array('class' => 'action-edit')
 375              );
 376          }
 377          // Copy.
 378          if (self::can_copy_course($course->id)) {
 379              $actions[] = array(
 380                  'url' => new \moodle_url('/backup/copy.php', array('id' => $course->id, 'returnto' => 'catmanage')),
 381                  'icon' => new \pix_icon('t/copy', \get_string('copycourse')),
 382                  'attributes' => array('class' => 'action-copy')
 383              );
 384          }
 385          // Delete.
 386          if ($course->can_delete()) {
 387              $actions[] = array(
 388                  'url' => new \moodle_url('/course/delete.php', array('id' => $course->id)),
 389                  'icon' => new \pix_icon('t/delete', \get_string('delete')),
 390                  'attributes' => array('class' => 'action-delete')
 391              );
 392          }
 393          // Show/Hide.
 394          if ($course->can_change_visibility()) {
 395              $actions[] = array(
 396                  'url' => new \moodle_url($baseurl, array('action' => 'hidecourse')),
 397                  'icon' => new \pix_icon('t/hide', \get_string('hide')),
 398                  'attributes' => array('data-action' => 'hide', 'class' => 'action-hide')
 399              );
 400              $actions[] = array(
 401                  'url' => new \moodle_url($baseurl, array('action' => 'showcourse')),
 402                  'icon' => new \pix_icon('t/show', \get_string('show')),
 403                  'attributes' => array('data-action' => 'show', 'class' => 'action-show')
 404              );
 405          }
 406          // Move up/down.
 407          if ($category->can_resort_courses()) {
 408              $actions[] = array(
 409                  'url' => new \moodle_url($baseurl, array('action' => 'movecourseup')),
 410                  'icon' => new \pix_icon('t/up', \get_string('moveup')),
 411                  'attributes' => array('data-action' => 'moveup', 'class' => 'action-moveup')
 412              );
 413              $actions[] = array(
 414                  'url' => new \moodle_url($baseurl, array('action' => 'movecoursedown')),
 415                  'icon' => new \pix_icon('t/down', \get_string('movedown')),
 416                  'attributes' => array('data-action' => 'movedown', 'class' => 'action-movedown')
 417              );
 418          }
 419          return $actions;
 420      }
 421  
 422      /**
 423       * Returns an array of actions that can be performed on the course being displayed.
 424       *
 425       * @param \core_course_list_element $course
 426       * @return array
 427       */
 428      public static function get_course_detail_actions(\core_course_list_element $course) {
 429          $params = array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => \sesskey());
 430          $baseurl = new \moodle_url('/course/management.php', $params);
 431          $actions = array();
 432          // View.
 433          $actions['view'] = array(
 434              'url' => new \moodle_url('/course/view.php', array('id' => $course->id)),
 435              'string' => \get_string('view')
 436          );
 437          // Edit.
 438          if ($course->can_edit()) {
 439              $actions['edit'] = array(
 440                  'url' => new \moodle_url('/course/edit.php', array('id' => $course->id)),
 441                  'string' => \get_string('edit')
 442              );
 443          }
 444          // Permissions.
 445          if ($course->can_review_enrolments()) {
 446              $actions['enrolledusers'] = array(
 447                  'url' => new \moodle_url('/user/index.php', array('id' => $course->id)),
 448                  'string' => \get_string('enrolledusers', 'enrol')
 449              );
 450          }
 451          // Delete.
 452          if ($course->can_delete()) {
 453              $actions['delete'] = array(
 454                  'url' => new \moodle_url('/course/delete.php', array('id' => $course->id)),
 455                  'string' => \get_string('delete')
 456              );
 457          }
 458          // Show/Hide.
 459          if ($course->can_change_visibility()) {
 460              if ($course->visible) {
 461                  $actions['hide'] = array(
 462                      'url' => new \moodle_url($baseurl, array('action' => 'hidecourse')),
 463                      'string' => \get_string('hide')
 464                  );
 465              } else {
 466                  $actions['show'] = array(
 467                      'url' => new \moodle_url($baseurl, array('action' => 'showcourse')),
 468                      'string' => \get_string('show')
 469                  );
 470              }
 471          }
 472          // Backup.
 473          if ($course->can_backup()) {
 474              $actions['backup'] = array(
 475                  'url' => new \moodle_url('/backup/backup.php', array('id' => $course->id)),
 476                  'string' => \get_string('backup')
 477              );
 478          }
 479          // Restore.
 480          if ($course->can_restore()) {
 481              $actions['restore'] = array(
 482                  'url' => new \moodle_url('/backup/restorefile.php', array('contextid' => $course->get_context()->id)),
 483                  'string' => \get_string('restore')
 484              );
 485          }
 486          return $actions;
 487      }
 488  
 489      /**
 490       * Resorts the courses within a category moving the given course up by one.
 491       *
 492       * @param \core_course_list_element $course
 493       * @param \core_course_category $category
 494       * @return bool
 495       * @throws \moodle_exception
 496       */
 497      public static function action_course_change_sortorder_up_one(\core_course_list_element $course,
 498                                                                   \core_course_category $category) {
 499          if (!$category->can_resort_courses()) {
 500              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
 501          }
 502          return \course_change_sortorder_by_one($course, true);
 503      }
 504  
 505      /**
 506       * Resorts the courses within a category moving the given course down by one.
 507       *
 508       * @param \core_course_list_element $course
 509       * @param \core_course_category $category
 510       * @return bool
 511       * @throws \moodle_exception
 512       */
 513      public static function action_course_change_sortorder_down_one(\core_course_list_element $course,
 514                                                                     \core_course_category $category) {
 515          if (!$category->can_resort_courses()) {
 516              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
 517          }
 518          return \course_change_sortorder_by_one($course, false);
 519      }
 520  
 521      /**
 522       * Resorts the courses within a category moving the given course up by one.
 523       *
 524       * @global \moodle_database $DB
 525       * @param int|\stdClass $courserecordorid
 526       * @return bool
 527       */
 528      public static function action_course_change_sortorder_up_one_by_record($courserecordorid) {
 529          if (is_int($courserecordorid)) {
 530              $courserecordorid = get_course($courserecordorid);
 531          }
 532          $course = new \core_course_list_element($courserecordorid);
 533          $category = \core_course_category::get($course->category);
 534          return self::action_course_change_sortorder_up_one($course, $category);
 535      }
 536  
 537      /**
 538       * Resorts the courses within a category moving the given course down by one.
 539       *
 540       * @global \moodle_database $DB
 541       * @param int|\stdClass $courserecordorid
 542       * @return bool
 543       */
 544      public static function action_course_change_sortorder_down_one_by_record($courserecordorid) {
 545          if (is_int($courserecordorid)) {
 546              $courserecordorid = get_course($courserecordorid);
 547          }
 548          $course = new \core_course_list_element($courserecordorid);
 549          $category = \core_course_category::get($course->category);
 550          return self::action_course_change_sortorder_down_one($course, $category);
 551      }
 552  
 553      /**
 554       * Changes the sort order so that the first course appears after the second course.
 555       *
 556       * @param int|\stdClass $courserecordorid
 557       * @param int $moveaftercourseid
 558       * @return bool
 559       * @throws \moodle_exception
 560       */
 561      public static function action_course_change_sortorder_after_course($courserecordorid, $moveaftercourseid) {
 562          $course = \get_course($courserecordorid);
 563          $category = \core_course_category::get($course->category);
 564          if (!$category->can_resort_courses()) {
 565              $url = '/course/management.php?categoryid='.$course->category;
 566              throw new \moodle_exception('nopermissions', 'error', $url, \get_string('resortcourses', 'moodle'));
 567          }
 568          return \course_change_sortorder_after_course($course, $moveaftercourseid);
 569      }
 570  
 571      /**
 572       * Makes a course visible given a \core_course_list_element object.
 573       *
 574       * @param \core_course_list_element $course
 575       * @return bool
 576       * @throws \moodle_exception
 577       */
 578      public static function action_course_show(\core_course_list_element $course) {
 579          if (!$course->can_change_visibility()) {
 580              throw new \moodle_exception('permissiondenied', 'error', '', null,
 581                  'core_course_list_element::can_change_visbility');
 582          }
 583          return course_change_visibility($course->id, true);
 584      }
 585  
 586      /**
 587       * Makes a course hidden given a \core_course_list_element object.
 588       *
 589       * @param \core_course_list_element $course
 590       * @return bool
 591       * @throws \moodle_exception
 592       */
 593      public static function action_course_hide(\core_course_list_element $course) {
 594          if (!$course->can_change_visibility()) {
 595              throw new \moodle_exception('permissiondenied', 'error', '', null,
 596                  'core_course_list_element::can_change_visbility');
 597          }
 598          return course_change_visibility($course->id, false);
 599      }
 600  
 601      /**
 602       * Makes a course visible given a course id or a database record.
 603       *
 604       * @global \moodle_database $DB
 605       * @param int|\stdClass $courserecordorid
 606       * @return bool
 607       */
 608      public static function action_course_show_by_record($courserecordorid) {
 609          if (is_int($courserecordorid)) {
 610              $courserecordorid = get_course($courserecordorid);
 611          }
 612          $course = new \core_course_list_element($courserecordorid);
 613          return self::action_course_show($course);
 614      }
 615  
 616      /**
 617       * Makes a course hidden given a course id or a database record.
 618       *
 619       * @global \moodle_database $DB
 620       * @param int|\stdClass $courserecordorid
 621       * @return bool
 622       */
 623      public static function action_course_hide_by_record($courserecordorid) {
 624          if (is_int($courserecordorid)) {
 625              $courserecordorid = get_course($courserecordorid);
 626          }
 627          $course = new \core_course_list_element($courserecordorid);
 628          return self::action_course_hide($course);
 629      }
 630  
 631      /**
 632       * Resort a categories subcategories shifting the given category up one.
 633       *
 634       * @param \core_course_category $category
 635       * @return bool
 636       * @throws \moodle_exception
 637       */
 638      public static function action_category_change_sortorder_up_one(\core_course_category $category) {
 639          if (!$category->can_change_sortorder()) {
 640              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_sortorder');
 641          }
 642          return $category->change_sortorder_by_one(true);
 643      }
 644  
 645      /**
 646       * Resort a categories subcategories shifting the given category down one.
 647       *
 648       * @param \core_course_category $category
 649       * @return bool
 650       * @throws \moodle_exception
 651       */
 652      public static function action_category_change_sortorder_down_one(\core_course_category $category) {
 653          if (!$category->can_change_sortorder()) {
 654              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_sortorder');
 655          }
 656          return $category->change_sortorder_by_one(false);
 657      }
 658  
 659      /**
 660       * Resort a categories subcategories shifting the given category up one.
 661       *
 662       * @param int $categoryid
 663       * @return bool
 664       */
 665      public static function action_category_change_sortorder_up_one_by_id($categoryid) {
 666          $category = \core_course_category::get($categoryid);
 667          return self::action_category_change_sortorder_up_one($category);
 668      }
 669  
 670      /**
 671       * Resort a categories subcategories shifting the given category down one.
 672       *
 673       * @param int $categoryid
 674       * @return bool
 675       */
 676      public static function action_category_change_sortorder_down_one_by_id($categoryid) {
 677          $category = \core_course_category::get($categoryid);
 678          return self::action_category_change_sortorder_down_one($category);
 679      }
 680  
 681      /**
 682       * Makes a category hidden given a core_course_category object.
 683       *
 684       * @param \core_course_category $category
 685       * @return bool
 686       * @throws \moodle_exception
 687       */
 688      public static function action_category_hide(\core_course_category $category) {
 689          if (!$category->can_change_visibility()) {
 690              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_visbility');
 691          }
 692          $category->hide();
 693          return true;
 694      }
 695  
 696      /**
 697       * Makes a category visible given a core_course_category object.
 698       *
 699       * @param \core_course_category $category
 700       * @return bool
 701       * @throws \moodle_exception
 702       */
 703      public static function action_category_show(\core_course_category $category) {
 704          if (!$category->can_change_visibility()) {
 705              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_visbility');
 706          }
 707          $category->show();
 708          return true;
 709      }
 710  
 711      /**
 712       * Makes a category visible given a course category id or database record.
 713       *
 714       * @param int|\stdClass $categoryid
 715       * @return bool
 716       */
 717      public static function action_category_show_by_id($categoryid) {
 718          return self::action_category_show(\core_course_category::get($categoryid));
 719      }
 720  
 721      /**
 722       * Makes a category hidden given a course category id or database record.
 723       *
 724       * @param int|\stdClass $categoryid
 725       * @return bool
 726       */
 727      public static function action_category_hide_by_id($categoryid) {
 728          return self::action_category_hide(\core_course_category::get($categoryid));
 729      }
 730  
 731      /**
 732       * Resorts the sub categories of the given category.
 733       *
 734       * @param \core_course_category $category
 735       * @param string $sort One of idnumber or name.
 736       * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
 737       * @return bool
 738       * @throws \moodle_exception
 739       */
 740      public static function action_category_resort_subcategories(\core_course_category $category, $sort, $cleanup = true) {
 741          if (!$category->can_resort_subcategories()) {
 742              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
 743          }
 744          return $category->resort_subcategories($sort, $cleanup);
 745      }
 746  
 747      /**
 748       * Resorts the courses within the given category.
 749       *
 750       * @param \core_course_category $category
 751       * @param string $sort One of fullname, shortname or idnumber
 752       * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
 753       * @return bool
 754       * @throws \moodle_exception
 755       */
 756      public static function action_category_resort_courses(\core_course_category $category, $sort, $cleanup = true) {
 757          if (!$category->can_resort_courses()) {
 758              throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
 759          }
 760          return $category->resort_courses($sort, $cleanup);
 761      }
 762  
 763      /**
 764       * Moves courses out of one category and into a new category.
 765       *
 766       * @param \core_course_category $oldcategory The category we are moving courses out of.
 767       * @param \core_course_category $newcategory The category we are moving courses into.
 768       * @param array $courseids The ID's of the courses we want to move.
 769       * @return bool True on success.
 770       * @throws \moodle_exception
 771       */
 772      public static function action_category_move_courses_into(\core_course_category $oldcategory,
 773                                                               \core_course_category $newcategory, array $courseids) {
 774          global $DB;
 775  
 776          list($where, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
 777          $params['categoryid'] = $oldcategory->id;
 778          $sql = "SELECT c.id FROM {course} c WHERE c.id {$where} AND c.category <> :categoryid";
 779          if ($DB->record_exists_sql($sql, $params)) {
 780              // Likely being tinkered with.
 781              throw new \moodle_exception('coursedoesnotbelongtocategory');
 782          }
 783  
 784          // All courses are currently within the old category.
 785          return self::move_courses_into_category($newcategory, $courseids);
 786      }
 787  
 788      /**
 789       * Returns the view modes for the management interface.
 790       * @return array
 791       */
 792      public static function get_management_viewmodes() {
 793          return array(
 794              'combined' => new \lang_string('categoriesandcourses'),
 795              'categories' => new \lang_string('categories'),
 796              'courses' => new \lang_string('courses')
 797          );
 798      }
 799  
 800      /**
 801       * Search for courses with matching params.
 802       *
 803       * Please note that only one of search, blocklist, or modulelist can be specified at a time.
 804       * Specifying more than one will result in only the first being used.
 805       *
 806       * @param string $search Words to search for. We search fullname, shortname, idnumber and summary.
 807       * @param int $blocklist The ID of a block, courses will only be returned if they use this block.
 808       * @param string $modulelist The name of a module (relates to database table name). Only courses containing this module
 809       *      will be returned.
 810       * @param int $page The page number to display, starting at 0.
 811       * @param int $perpage The number of courses to display per page.
 812       * @return array
 813       */
 814      public static function search_courses($search, $blocklist, $modulelist, $page = 0, $perpage = null) {
 815          global $CFG;
 816  
 817          if ($perpage === null) {
 818              $perpage = $CFG->coursesperpage;
 819          }
 820  
 821          $searchcriteria = array();
 822          if (!empty($search)) {
 823              $searchcriteria = array('search' => $search);
 824          } else if (!empty($blocklist)) {
 825              $searchcriteria = array('blocklist' => $blocklist);
 826          } else if (!empty($modulelist)) {
 827              $searchcriteria = array('modulelist' => $modulelist);
 828          }
 829  
 830          $topcat = \core_course_category::top();
 831          $courses = $topcat->search_courses($searchcriteria, array(
 832              'recursive' => true,
 833              'offset' => $page * $perpage,
 834              'limit' => $perpage,
 835              'sort' => array('fullname' => 1)
 836          ));
 837          $totalcount = $topcat->search_courses_count($searchcriteria, array('recursive' => true));
 838  
 839          return array($courses, \count($courses), $totalcount);
 840      }
 841  
 842      /**
 843       * Moves one or more courses out of the category they are currently in and into a new category.
 844       *
 845       * This function works much the same way as action_category_move_courses_into however it allows courses from multiple
 846       * categories to be moved into a single category.
 847       *
 848       * @param int|\core_course_category $categoryorid The category to move them into.
 849       * @param array|int $courseids An array of course id's or optionally just a single course id.
 850       * @return bool True on success or false on failure.
 851       * @throws \moodle_exception
 852       */
 853      public static function move_courses_into_category($categoryorid, $courseids = array()) {
 854          global $DB;
 855          if (!is_array($courseids)) {
 856              // Just a single course ID.
 857              $courseids = array($courseids);
 858          }
 859          // Bulk move courses from one category to another.
 860          if (count($courseids) === 0) {
 861              return false;
 862          }
 863          if ($categoryorid instanceof \core_course_category) {
 864              $moveto = $categoryorid;
 865          } else {
 866              $moveto = \core_course_category::get($categoryorid);
 867          }
 868          if (!$moveto->can_move_courses_out_of() || !$moveto->can_move_courses_into()) {
 869              throw new \moodle_exception('cannotmovecourses');
 870          }
 871  
 872          list($where, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
 873          $sql = "SELECT c.id, c.category FROM {course} c WHERE c.id {$where}";
 874          $courses = $DB->get_records_sql($sql, $params);
 875          $checks = array();
 876          foreach ($courseids as $id) {
 877              if (!isset($courses[$id])) {
 878                  throw new \moodle_exception('invalidcourseid');
 879              }
 880              $catid = $courses[$id]->category;
 881              if (!isset($checks[$catid])) {
 882                  $coursecat = \core_course_category::get($catid);
 883                  $checks[$catid] = $coursecat->can_move_courses_out_of() && $coursecat->can_move_courses_into();
 884              }
 885              if (!$checks[$catid]) {
 886                  throw new \moodle_exception('cannotmovecourses');
 887              }
 888          }
 889          return \move_courses($courseids, $moveto->id);
 890      }
 891  
 892      /**
 893       * Returns an array of courseids and visiblity for all courses within the given category.
 894       * @param int $categoryid
 895       * @return array
 896       */
 897      public static function get_category_courses_visibility($categoryid) {
 898          global $DB;
 899          $sql = "SELECT c.id, c.visible
 900                    FROM {course} c
 901                   WHERE c.category = :category";
 902          $params = array('category' => (int)$categoryid);
 903          return $DB->get_records_sql($sql, $params);
 904      }
 905  
 906      /**
 907       * Returns an array of all categoryids that have the given category as a parent and their visible value.
 908       * @param int $categoryid
 909       * @return array
 910       */
 911      public static function get_category_children_visibility($categoryid) {
 912          global $DB;
 913          $category = \core_course_category::get($categoryid);
 914          $select = $DB->sql_like('path', ':path');
 915          $path = $category->path . '/%';
 916  
 917          $sql = "SELECT c.id, c.visible
 918                    FROM {course_categories} c
 919                   WHERE ".$select;
 920          $params = array('path' => $path);
 921          return $DB->get_records_sql($sql, $params);
 922      }
 923  
 924      /**
 925       * Records when a category is expanded or collapsed so that when the user
 926       *
 927       * @param \core_course_category $coursecat The category we're working with.
 928       * @param bool $expanded True if the category is expanded now.
 929       */
 930      public static function record_expanded_category(\core_course_category $coursecat, $expanded = true) {
 931          // If this ever changes we are going to reset it and reload the categories as required.
 932          self::$expandedcategories = null;
 933          $categoryid = $coursecat->id;
 934          $path = $coursecat->get_parents();
 935          /* @var \cache_session $cache */
 936          $cache = \cache::make('core', 'userselections');
 937          $categories = $cache->get('categorymanagementexpanded');
 938          if (!is_array($categories)) {
 939              if (!$expanded) {
 940                  // No categories recorded, nothing to remove.
 941                  return;
 942              }
 943              $categories = array();
 944          }
 945          if ($expanded) {
 946              $ref =& $categories;
 947              foreach ($coursecat->get_parents() as $path) {
 948                  if (!isset($ref[$path]) || !is_array($ref[$path])) {
 949                      $ref[$path] = array();
 950                  }
 951                  $ref =& $ref[$path];
 952              }
 953              if (!isset($ref[$categoryid])) {
 954                  $ref[$categoryid] = true;
 955              }
 956          } else {
 957              $found = true;
 958              $ref =& $categories;
 959              foreach ($coursecat->get_parents() as $path) {
 960                  if (!isset($ref[$path])) {
 961                      $found = false;
 962                      break;
 963                  }
 964                  $ref =& $ref[$path];
 965              }
 966              if ($found) {
 967                  $ref[$categoryid] = null;
 968                  unset($ref[$categoryid]);
 969              }
 970          }
 971          $cache->set('categorymanagementexpanded', $categories);
 972      }
 973  
 974      /**
 975       * Returns the categories that should be expanded when displaying the interface.
 976       *
 977       * @param int|null $withpath If specified a path to require as the parent.
 978       * @return \core_course_category[] An array of Category ID's to expand.
 979       */
 980      public static function get_expanded_categories($withpath = null) {
 981          if (self::$expandedcategories === null) {
 982              /* @var \cache_session $cache */
 983              $cache = \cache::make('core', 'userselections');
 984              self::$expandedcategories = $cache->get('categorymanagementexpanded');
 985              if (self::$expandedcategories === false) {
 986                  self::$expandedcategories = array();
 987              }
 988          }
 989          if (empty($withpath)) {
 990              return array_keys(self::$expandedcategories);
 991          }
 992          $parents = explode('/', trim($withpath, '/'));
 993          $ref =& self::$expandedcategories;
 994          foreach ($parents as $parent) {
 995              if (!isset($ref[$parent])) {
 996                  return array();
 997              }
 998              $ref =& $ref[$parent];
 999          }
1000          if (is_array($ref)) {
1001              return array_keys($ref);
1002          } else {
1003              return array($parent);
1004          }
1005      }
1006  
1007      /**
1008       * Get an array of the capabilities required to copy a course.
1009       *
1010       * @return array
1011       */
1012      public static function get_course_copy_capabilities(): array {
1013          return array('moodle/backup:backupcourse', 'moodle/restore:restorecourse', 'moodle/course:create');
1014      }
1015  
1016      /**
1017       * Returns true if the current user can copy this course.
1018       *
1019       * @param int $courseid
1020       * @return bool
1021       */
1022      public static function can_copy_course(int $courseid): bool {
1023          $coursecontext = \context_course::instance($courseid);
1024          return has_all_capabilities(self::get_course_copy_capabilities(), $coursecontext);
1025      }
1026  }