Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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