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
25namespace core_course\management;
26
27defined('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 */
43class 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}
1028