Search moodle.org's
Developer Documentation

  • 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 37 and 311] [Versions 38 and 311]

       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  }