From 3ec163ddf16936b61a8a5da76a5261e5657f651f Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Thu, 17 May 2012 22:13:28 +0200 Subject: [PATCH] MDL-32941 Reorganise functions a bit --- course/externallib.php | 1202 ++++++++++++++++++++-------------------- lib/db/services.php | 46 +- 2 files changed, 632 insertions(+), 616 deletions(-) diff --git a/course/externallib.php b/course/externallib.php index dc2f7254ac3..d770ac800db 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -254,298 +254,6 @@ class core_course_external extends external_api { * @return external_function_parameters * @since Moodle 2.3 */ - public static function get_categories_parameters() { - return new external_function_parameters( - array( - 'criteria' => new external_multiple_structure( - new external_single_structure( - array( - 'key' => new external_value(PARAM_ALPHA, - 'The category column to search, expected keys (value format) are:'. - '"id" (int) the category id,'. - '"name" (string) the category name,'. - '"parent" (int) the parent category id,'. - '"idnumber" (string) category idnumber'. - ' - user must have \'moodle/category:manage\' to search on idnumber,'. - '"visible" (int) whether the category is visible or not'. - ' - user must have \'moodle/category:manage\' or \'moodle/category:viewhiddencategories\' to search on visible,'. - '"theme" (string) category theme'. - ' - user must have \'moodle/category:manage\' to search on theme'), - 'value' => new external_value(PARAM_RAW, 'the value to match') - ) - ), VALUE_DEFAULT, array() - ), - 'addsubcategories' => new external_value(PARAM_BOOL, 'return the sub categories infos - (1 - default) otherwise only the category info (0)', VALUE_DEFAULT, 1) - ) - ); - } - - /** - * Get categories - * - * @param array $criteria Criteria to match the results - * @param booln $addsubcategories obtain only the category (false) or its subcategories (true - default) - * @return array list of categories - * @since Moodle 2.3 - */ - public static function get_categories($criteria = array(), $addsubcategories = true) { - global $CFG, $DB; - require_once($CFG->dirroot . "/course/lib.php"); - - // Validate parameters. - $params = self::validate_parameters(self::get_categories_parameters(), - array('criteria' => $criteria, 'addsubcategories' => $addsubcategories)); - - // Retrieve the categories. - $categories = array(); - if (!empty($params['criteria'])) { - - $conditions = array(); - $wheres = array(); - foreach ($params['criteria'] as $crit) { - $key = trim($crit['key']); - - // Trying to avoid duplicate keys. - if (!isset($conditions[$key])) { - - $context = context_system::instance(); - $value = null; - switch ($key) { - case 'id': - $value = clean_param($crit['value'], PARAM_INT); - break; - - case 'idnumber': - if (has_capability('moodle/category:manage', $context)) { - $value = clean_param($crit['value'], PARAM_RAW); - } else { - // We must throw an exception. - // Otherwise the dev client would think no idnumber exists. - throw new moodle_exception('criteriaerror', - 'webservice', '', null, - 'You don\'t have the permissions to search on the "idnumber" field.'); - } - break; - - case 'name': - $value = clean_param($crit['value'], PARAM_TEXT); - break; - - case 'parent': - $value = clean_param($crit['value'], PARAM_INT); - break; - - case 'visible': - if (has_capability('moodle/category:manage', $context) - or has_capability('moodle/category:viewhiddencategories', - context_system::instance())) { - $value = clean_param($crit['value'], PARAM_INT); - } else { - throw new moodle_exception('criteriaerror', - 'webservice', '', null, - 'You don\'t have the permissions to search on the "visible" field.'); - } - break; - - case 'theme': - if (has_capability('moodle/category:manage', $context)) { - $value = clean_param($crit['value'], PARAM_THEME); - } else { - throw new moodle_exception('criteriaerror', - 'webservice', '', null, - 'You don\'t have the permissions to search on the "theme" field.'); - } - break; - - default: - throw new moodle_exception('criteriaerror', - 'webservice', '', null, - 'You can not search on this criteria: ' . $key); - } - - if (isset($value)) { - $conditions[$key] = $crit['value']; - $wheres[] = $key . " = :" . $key; - } - } - } - - if (!empty($wheres)) { - $wheres = implode(" AND ", $wheres); - - $categories = $DB->get_records_select('course_categories', $wheres, $conditions); - - // Retrieve its sub subcategories (all levels). - if ($categories and !empty($params['addsubcategories'])) { - $newcategories = array(); - - foreach ($categories as $category) { - $sqllike = $DB->sql_like('path', ':path'); - $sqlparams = array('path' => $category->path.'/%'); // It will NOT include the specified category. - $subcategories = $DB->get_records_select('course_categories', $sqllike, $sqlparams); - $newcategories = $newcategories + $subcategories; // Both arrays have integer as keys. - } - $categories = $categories + $newcategories; - } - } - - } else { - // Retrieve all categories in the database. - $categories = $DB->get_records('course_categories'); - } - - // The not returned categories. key => category id, value => reason of exclusion. - $excludedcats = array(); - - // The returned categories. - $categoriesinfo = array(); - - // We need to sort the categories by path. - // The parent cats need to be checked by the algo first. - usort($categories, "core_course_external::compare_categories_by_path"); - - foreach ($categories as $category) { - - // Check if the category is a child of an excluded category, if yes exclude it too (excluded => do not return). - $parents = explode('/', $category->path); - unset($parents[0]); // First key is always empty because path start with / => /1/2/4. - foreach ($parents as $parentid) { - // Note: when the parent exclusion was due to the context, - // the sub category could still be returned. - if (isset($excludedcats[$parentid]) and $excludedcats[$parentid] != 'context') { - $excludedcats[$category->id] = 'parent'; - } - } - - // Check category depth is <= maxdepth (do not check for user who can manage categories). - if ((!empty($CFG->maxcategorydepth) && count($parents) > $CFG->maxcategorydepth) - and !has_capability('moodle/category:manage', $context)) { - $excludedcats[$category->id] = 'depth'; - } - - // Check the user can use the category context. - $context = context_coursecat::instance($category->id); - try { - self::validate_context($context); - } catch (Exception $e) { - $excludedcats[$category->id] = 'context'; - - // If it was the requested category then throw an exception. - if (isset($params['categoryid']) && $category->id == $params['categoryid']) { - $exceptionparam = new stdClass(); - $exceptionparam->message = $e->getMessage(); - $exceptionparam->catid = $category->id; - throw new moodle_exception('errorcatcontextnotvalid', 'webservice', '', $exceptionparam); - } - } - - // Return the category information. - if (!isset($excludedcats[$category->id])) { - - // Final check to see if the category is visible to the user. - if ($category->visible - or has_capability('moodle/category:viewhiddencategories', context_system::instance()) - or has_capability('moodle/category:manage', $context)) { - - $categoryinfo = array(); - $categoryinfo['id'] = $category->id; - $categoryinfo['name'] = $category->name; - $categoryinfo['description'] = file_rewrite_pluginfile_urls($category->description, - 'webservice/pluginfile.php', $context->id, 'coursecat', 'description', null); - $options = new stdClass; - $options->noclean = true; - $options->para = false; - $categoryinfo['description'] = format_text($categoryinfo['description'], - $category->descriptionformat, $options); - $categoryinfo['parent'] = $category->parent; - $categoryinfo['sortorder'] = $category->sortorder; - $categoryinfo['coursecount'] = $category->coursecount; - $categoryinfo['depth'] = $category->depth; - $categoryinfo['path'] = $category->path; - - // Some fields only returned for admin. - if (has_capability('moodle/category:manage', $context)) { - $categoryinfo['idnumber'] = $category->idnumber; - $categoryinfo['visible'] = $category->visible; - $categoryinfo['visibleold'] = $category->visibleold; - $categoryinfo['timemodified'] = $category->timemodified; - $categoryinfo['theme'] = $category->theme; - } - - $categoriesinfo[] = $categoryinfo; - } else { - $excludedcats[$category->id] = 'visibility'; - } - } - } - - // Sorting the resulting array so it looks a bit better for the client developer. - usort($categoriesinfo, "core_course_external::compare_categories_by_sortorder"); - - return $categoriesinfo; - } - - /** - * Sort categories array by path - * private function: only used by get_categories - * - * @param array $category1 - * @param array $category2 - * @return int result of strcmp - * @since Moodle 2.3 - */ - private static function compare_categories_by_path($category1, $category2) { - return strcmp($category1->path, $category2->path); - } - - /** - * Sort categories array by sortorder - * private function: only used by get_categories - * - * @param array $category1 - * @param array $category2 - * @return int result of strcmp - * @since Moodle 2.3 - */ - private static function compare_categories_by_sortorder($category1, $category2) { - return strcmp($category1['sortorder'], $category2['sortorder']); - } - - /** - * Returns description of method result value - * - * @return external_description - * @since Moodle 2.3 - */ - public static function get_categories_returns() { - return new external_multiple_structure( - new external_single_structure( - array( - 'id' => new external_value(PARAM_INT, 'category id'), - 'name' => new external_value(PARAM_TEXT, 'category name'), - 'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL), - 'description' => new external_value(PARAM_RAW, 'category description'), - 'parent' => new external_value(PARAM_INT, 'parent category id'), - 'sortorder' => new external_value(PARAM_INT, 'category sorting order'), - 'coursecount' => new external_value(PARAM_INT, 'number of courses in this category'), - 'visible' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), - 'visibleold' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), - 'timemodified' => new external_value(PARAM_INT, 'timestamp', VALUE_OPTIONAL), - 'depth' => new external_value(PARAM_INT, 'category depth'), - 'path' => new external_value(PARAM_TEXT, 'category path'), - 'theme' => new external_value(PARAM_THEME, 'category theme', VALUE_OPTIONAL), - ), 'List of categories' - ) - ); - } - - /** - * Returns description of method parameters - * - * @return external_function_parameters - * @since Moodle 2.2 - */ public static function get_courses_parameters() { return new external_function_parameters( array('options' => new external_single_structure( @@ -680,150 +388,33 @@ class core_course_external extends external_api { '1: available to student, 0:not available', VALUE_OPTIONAL), 'hiddensections' => new external_value(PARAM_INT, 'How the hidden sections in the course are displayed to students', - VALUE_OPTIONAL), - 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', - VALUE_OPTIONAL), - 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', - VALUE_OPTIONAL), - 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', - VALUE_OPTIONAL), - 'timecreated' => new external_value(PARAM_INT, - 'timestamp when the course have been created', VALUE_OPTIONAL), - 'timemodified' => new external_value(PARAM_INT, - 'timestamp when the course have been modified', VALUE_OPTIONAL), - 'enablecompletion' => new external_value(PARAM_INT, - 'Enabled, control via completion and activity settings. Disbaled, - not shown in activity settings.', - VALUE_OPTIONAL), - 'completionstartonenrol' => new external_value(PARAM_INT, - '1: begin tracking a student\'s progress in course completion - after course enrolment. 0: does not', - VALUE_OPTIONAL), - 'completionnotify' => new external_value(PARAM_INT, - '1: yes 0: no', VALUE_OPTIONAL), - 'lang' => new external_value(PARAM_SAFEDIR, - 'forced course language', VALUE_OPTIONAL), - 'forcetheme' => new external_value(PARAM_PLUGIN, - 'name of the force theme', VALUE_OPTIONAL), - ), 'course' - ) - ); - } - - /** - * Returns description of method parameters - * - * @return external_function_parameters - * @since Moodle 2.3 - */ - public static function create_categories_parameters() { - return new external_function_parameters( - array( - 'categories' => new external_multiple_structure( - new external_single_structure( - array( - 'name' => new external_value(PARAM_TEXT, 'new category name'), - 'parent' => new external_value(PARAM_INT, - 'the parent category id inside which the new category will be created'), - 'idnumber' => new external_value(PARAM_RAW, - 'the new category idnumber', VALUE_OPTIONAL), - 'description' => new external_value(PARAM_RAW, - 'the new category description', VALUE_OPTIONAL), - 'theme' => new external_value(PARAM_THEME, - 'the new category theme. This option must be enabled on moodle', - VALUE_OPTIONAL), - ) - ) - ) - ) - ); - } - - /** - * Create categories - * - * @param array $categories - see create_categories_parameters() for the array structure - * @return array - see create_categories_returns() for the array structure - * @since Moodle 2.3 - */ - public static function create_categories($categories) { - global $CFG, $DB; - require_once($CFG->dirroot . "/course/lib.php"); - - $params = self::validate_parameters(self::create_categories_parameters(), - array('categories' => $categories)); - - $transaction = $DB->start_delegated_transaction(); - - $createdcategories = array(); - foreach ($params['categories'] as $category) { - if ($category['parent']) { - if (!$DB->record_exists('course_categories', array('id' => $category['parent']))) { - throw new moodle_exception('unknowcategory'); - } - $context = context_coursecat::instance($category['parent']); - } else { - $context = context_system::instance(); - } - self::validate_context($context); - require_capability('moodle/category:manage', $context); - - // Check id number. - if (!empty($category['idnumber'])) { // Same as in course/editcategory_form.php . - if (textlib::strlen($category['idnumber'])>100) { - throw new moodle_exception('idnumbertoolong'); - } - if ($existing = $DB->get_record('course_categories', array('idnumber' => $category['idnumber']))) { - if ($existing->id) { - throw new moodle_exception('idnumbertaken'); - } - } - } - // Check name. - if (textlib::strlen($category['name'])>255) { - throw new moodle_exception('categorytoolong'); - } - - $newcategory = new stdClass(); - $newcategory->name = $category['name']; - $newcategory->parent = $category['parent']; - $newcategory->idnumber = $category['idnumber']; - $newcategory->sortorder = 999; // Same as in the course/editcategory.php . - // Format the description. - if (!empty($category['description'])) { - $newcategory->description = $category['description']; - } - $newcategory->descriptionformat = FORMAT_HTML; - if (isset($category['theme']) and !empty($CFG->allowcategorythemes)) { - $newcategory->theme = $category['theme']; - } - - $newcategory = create_course_category($newcategory); - // Populate special fields. - fix_course_sortorder(); - - $createdcategories[] = array('id' => $newcategory->id, 'name' => $newcategory->name); - } - - $transaction->allow_commit(); - - return $createdcategories; - } - - /** - * Returns description of method parameters - * - * @return external_function_parameters - * @since Moodle 2.3 - */ - public static function create_categories_returns() { - return new external_multiple_structure( - new external_single_structure( - array( - 'id' => new external_value(PARAM_INT, 'new category id'), - 'name' => new external_value(PARAM_TEXT, 'new category name'), + VALUE_OPTIONAL), + 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', + VALUE_OPTIONAL), + 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', + VALUE_OPTIONAL), + 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', + VALUE_OPTIONAL), + 'timecreated' => new external_value(PARAM_INT, + 'timestamp when the course have been created', VALUE_OPTIONAL), + 'timemodified' => new external_value(PARAM_INT, + 'timestamp when the course have been modified', VALUE_OPTIONAL), + 'enablecompletion' => new external_value(PARAM_INT, + 'Enabled, control via completion and activity settings. Disbaled, + not shown in activity settings.', + VALUE_OPTIONAL), + 'completionstartonenrol' => new external_value(PARAM_INT, + '1: begin tracking a student\'s progress in course completion + after course enrolment. 0: does not', + VALUE_OPTIONAL), + 'completionnotify' => new external_value(PARAM_INT, + '1: yes 0: no', VALUE_OPTIONAL), + 'lang' => new external_value(PARAM_SAFEDIR, + 'forced course language', VALUE_OPTIONAL), + 'forcetheme' => new external_value(PARAM_PLUGIN, + 'name of the force theme', VALUE_OPTIONAL), + ), 'course' ) - ) ); } @@ -1003,7 +594,9 @@ class core_course_external extends external_api { /** * Returns description of method parameters + * * @return external_function_parameters + * @since Moodle 2.2 */ public static function delete_courses_parameters() { return new external_function_parameters( @@ -1015,7 +608,9 @@ class core_course_external extends external_api { /** * Delete courses + * * @param array $courseids A list of course ids + * @since Moodle 2.2 */ public static function delete_courses($courseids) { global $CFG, $DB; @@ -1049,7 +644,9 @@ class core_course_external extends external_api { /** * Returns description of method result value + * * @return external_description + * @since Moodle 2.2 */ public static function delete_courses_returns() { return null; @@ -1123,155 +720,434 @@ class core_course_external extends external_api { ) ); - // Context validation. + // Context validation. + + if (! ($course = $DB->get_record('course', array('id'=>$params['courseid'])))) { + throw new moodle_exception('invalidcourseid', 'error', '', $params['courseid']); + } + + // Category where duplicated course is going to be created. + $categorycontext = context_coursecat::instance($params['categoryid']); + self::validate_context($categorycontext); + + // Course to be duplicated. + $coursecontext = context_course::instance($course->id); + self::validate_context($coursecontext); + + $backupdefaults = array( + 'activities' => 1, + 'blocks' => 1, + 'filters' => 1, + 'users' => 0, + 'role_assignments' => 0, + 'user_files' => 0, + 'comments' => 0, + 'completion_information' => 0, + 'logs' => 0, + 'histories' => 0 + ); + + $backupsettings = array(); + // Check for backup and restore options. + if (!empty($params['options'])) { + foreach ($params['options'] as $option) { + + // Strict check for a correct value (allways 1 or 0, true or false). + $value = clean_param($option['value'], PARAM_INT); + + if ($value !== 0 and $value !== 1) { + throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); + } + + if (!isset($backupdefaults[$option['name']])) { + throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); + } + + $backupsettings[$option['name']] = $value; + } + } + + // Capability checking. + + // The backup controller check for this currently, this may be redundant. + require_capability('moodle/course:create', $categorycontext); + require_capability('moodle/restore:restorecourse', $categorycontext); + require_capability('moodle/backup:backupcourse', $coursecontext); + + if (!empty($backupsettings['users'])) { + require_capability('moodle/backup:userinfo', $coursecontext); + require_capability('moodle/restore:userinfo', $categorycontext); + } + + // Check if the shortname is used. + if ($foundcourses = $DB->get_records('course', array('shortname'=>$shortname))) { + foreach ($foundcourses as $foundcourse) { + $foundcoursenames[] = $foundcourse->fullname; + } + + $foundcoursenamestring = implode(',', $foundcoursenames); + throw new moodle_exception('shortnametaken', '', '', $foundcoursenamestring); + } + + // Backup the course. + + $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id); + + foreach ($backupsettings as $name => $value) { + $bc->get_plan()->get_setting($name)->set_value($value); + } + + $backupid = $bc->get_backupid(); + $backupbasepath = $bc->get_plan()->get_basepath(); + + $bc->execute_plan(); + $results = $bc->get_results(); + $file = $results['backup_destination']; + + $bc->destroy(); + + // Restore the backup immediately. + + // Check if we need to unzip the file because the backup temp dir does not contains backup files. + if (!file_exists($backupbasepath . "/moodle_backup.xml")) { + $file->extract_to_pathname(get_file_packer(), $backupbasepath); + } + + // Create new course. + $newcourseid = restore_dbops::create_new_course($params['fullname'], $params['shortname'], $params['categoryid']); + + $rc = new restore_controller($backupid, $newcourseid, + backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_NEW_COURSE); + + foreach ($backupsettings as $name => $value) { + $setting = $rc->get_plan()->get_setting($name); + if ($setting->get_status() == backup_setting::NOT_LOCKED) { + $setting->set_value($value); + } + } + + if (!$rc->execute_precheck()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + $errorinfo = ''; + + foreach ($precheckresults['errors'] as $error) { + $errorinfo .= $error; + } + + if (array_key_exists('warnings', $precheckresults)) { + foreach ($precheckresults['warnings'] as $warning) { + $errorinfo .= $warning; + } + } + + throw new moodle_exception('backupprecheckerrors', 'webservice', '', $errorinfo); + } + } + + $rc->execute_plan(); + $rc->destroy(); + + $course = $DB->get_record('course', array('id' => $newcourseid), '*', MUST_EXIST); + $course->fullname = $params['fullname']; + $course->shortname = $params['shortname']; + $course->visible = $params['visible']; + + // Set shortname and fullname back. + $DB->update_record('course', $course); + + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + // Delete the course backup file created by this WebService. Originally located in the course backups area. + $file->delete(); + + return array('id' => $course->id, 'shortname' => $course->shortname); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.3 + */ + public static function duplicate_course_returns() { + return new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'course id'), + 'shortname' => new external_value(PARAM_TEXT, 'short name'), + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function get_categories_parameters() { + return new external_function_parameters( + array( + 'criteria' => new external_multiple_structure( + new external_single_structure( + array( + 'key' => new external_value(PARAM_ALPHA, + 'The category column to search, expected keys (value format) are:'. + '"id" (int) the category id,'. + '"name" (string) the category name,'. + '"parent" (int) the parent category id,'. + '"idnumber" (string) category idnumber'. + ' - user must have \'moodle/category:manage\' to search on idnumber,'. + '"visible" (int) whether the category is visible or not'. + ' - user must have \'moodle/category:manage\' or \'moodle/category:viewhiddencategories\' to search on visible,'. + '"theme" (string) category theme'. + ' - user must have \'moodle/category:manage\' to search on theme'), + 'value' => new external_value(PARAM_RAW, 'the value to match') + ) + ), VALUE_DEFAULT, array() + ), + 'addsubcategories' => new external_value(PARAM_BOOL, 'return the sub categories infos + (1 - default) otherwise only the category info (0)', VALUE_DEFAULT, 1) + ) + ); + } + + /** + * Get categories + * + * @param array $criteria Criteria to match the results + * @param booln $addsubcategories obtain only the category (false) or its subcategories (true - default) + * @return array list of categories + * @since Moodle 2.3 + */ + public static function get_categories($criteria = array(), $addsubcategories = true) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + // Validate parameters. + $params = self::validate_parameters(self::get_categories_parameters(), + array('criteria' => $criteria, 'addsubcategories' => $addsubcategories)); + + // Retrieve the categories. + $categories = array(); + if (!empty($params['criteria'])) { + + $conditions = array(); + $wheres = array(); + foreach ($params['criteria'] as $crit) { + $key = trim($crit['key']); + + // Trying to avoid duplicate keys. + if (!isset($conditions[$key])) { - if (! ($course = $DB->get_record('course', array('id'=>$params['courseid'])))) { - throw new moodle_exception('invalidcourseid', 'error', '', $params['courseid']); - } + $context = context_system::instance(); + $value = null; + switch ($key) { + case 'id': + $value = clean_param($crit['value'], PARAM_INT); + break; - // Category where duplicated course is going to be created. - $categorycontext = context_coursecat::instance($params['categoryid']); - self::validate_context($categorycontext); + case 'idnumber': + if (has_capability('moodle/category:manage', $context)) { + $value = clean_param($crit['value'], PARAM_RAW); + } else { + // We must throw an exception. + // Otherwise the dev client would think no idnumber exists. + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You don\'t have the permissions to search on the "idnumber" field.'); + } + break; - // Course to be duplicated. - $coursecontext = context_course::instance($course->id); - self::validate_context($coursecontext); + case 'name': + $value = clean_param($crit['value'], PARAM_TEXT); + break; - $backupdefaults = array( - 'activities' => 1, - 'blocks' => 1, - 'filters' => 1, - 'users' => 0, - 'role_assignments' => 0, - 'user_files' => 0, - 'comments' => 0, - 'completion_information' => 0, - 'logs' => 0, - 'histories' => 0 - ); + case 'parent': + $value = clean_param($crit['value'], PARAM_INT); + break; - $backupsettings = array(); - // Check for backup and restore options. - if (!empty($params['options'])) { - foreach ($params['options'] as $option) { + case 'visible': + if (has_capability('moodle/category:manage', $context) + or has_capability('moodle/category:viewhiddencategories', + context_system::instance())) { + $value = clean_param($crit['value'], PARAM_INT); + } else { + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You don\'t have the permissions to search on the "visible" field.'); + } + break; - // Strict check for a correct value (allways 1 or 0, true or false). - $value = clean_param($option['value'], PARAM_INT); + case 'theme': + if (has_capability('moodle/category:manage', $context)) { + $value = clean_param($crit['value'], PARAM_THEME); + } else { + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You don\'t have the permissions to search on the "theme" field.'); + } + break; - if ($value !== 0 and $value !== 1) { - throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); - } + default: + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You can not search on this criteria: ' . $key); + } - if (!isset($backupdefaults[$option['name']])) { - throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); + if (isset($value)) { + $conditions[$key] = $crit['value']; + $wheres[] = $key . " = :" . $key; + } } - - $backupsettings[$option['name']] = $value; } - } - // Capability checking. + if (!empty($wheres)) { + $wheres = implode(" AND ", $wheres); - // The backup controller check for this currently, this may be redundant. - require_capability('moodle/course:create', $categorycontext); - require_capability('moodle/restore:restorecourse', $categorycontext); - require_capability('moodle/backup:backupcourse', $coursecontext); + $categories = $DB->get_records_select('course_categories', $wheres, $conditions); - if (!empty($backupsettings['users'])) { - require_capability('moodle/backup:userinfo', $coursecontext); - require_capability('moodle/restore:userinfo', $categorycontext); - } + // Retrieve its sub subcategories (all levels). + if ($categories and !empty($params['addsubcategories'])) { + $newcategories = array(); - // Check if the shortname is used. - if ($foundcourses = $DB->get_records('course', array('shortname'=>$shortname))) { - foreach ($foundcourses as $foundcourse) { - $foundcoursenames[] = $foundcourse->fullname; + foreach ($categories as $category) { + $sqllike = $DB->sql_like('path', ':path'); + $sqlparams = array('path' => $category->path.'/%'); // It will NOT include the specified category. + $subcategories = $DB->get_records_select('course_categories', $sqllike, $sqlparams); + $newcategories = $newcategories + $subcategories; // Both arrays have integer as keys. + } + $categories = $categories + $newcategories; + } } - $foundcoursenamestring = implode(',', $foundcoursenames); - throw new moodle_exception('shortnametaken', '', '', $foundcoursenamestring); - } - - // Backup the course. - - $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, - backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id); - - foreach ($backupsettings as $name => $value) { - $bc->get_plan()->get_setting($name)->set_value($value); + } else { + // Retrieve all categories in the database. + $categories = $DB->get_records('course_categories'); } - $backupid = $bc->get_backupid(); - $backupbasepath = $bc->get_plan()->get_basepath(); + // The not returned categories. key => category id, value => reason of exclusion. + $excludedcats = array(); - $bc->execute_plan(); - $results = $bc->get_results(); - $file = $results['backup_destination']; + // The returned categories. + $categoriesinfo = array(); - $bc->destroy(); + // We need to sort the categories by path. + // The parent cats need to be checked by the algo first. + usort($categories, "core_course_external::compare_categories_by_path"); - // Restore the backup immediately. + foreach ($categories as $category) { - // Check if we need to unzip the file because the backup temp dir does not contains backup files. - if (!file_exists($backupbasepath . "/moodle_backup.xml")) { - $file->extract_to_pathname(get_file_packer(), $backupbasepath); - } + // Check if the category is a child of an excluded category, if yes exclude it too (excluded => do not return). + $parents = explode('/', $category->path); + unset($parents[0]); // First key is always empty because path start with / => /1/2/4. + foreach ($parents as $parentid) { + // Note: when the parent exclusion was due to the context, + // the sub category could still be returned. + if (isset($excludedcats[$parentid]) and $excludedcats[$parentid] != 'context') { + $excludedcats[$category->id] = 'parent'; + } + } - // Create new course. - $newcourseid = restore_dbops::create_new_course($params['fullname'], $params['shortname'], $params['categoryid']); + // Check category depth is <= maxdepth (do not check for user who can manage categories). + if ((!empty($CFG->maxcategorydepth) && count($parents) > $CFG->maxcategorydepth) + and !has_capability('moodle/category:manage', $context)) { + $excludedcats[$category->id] = 'depth'; + } - $rc = new restore_controller($backupid, $newcourseid, - backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_NEW_COURSE); + // Check the user can use the category context. + $context = context_coursecat::instance($category->id); + try { + self::validate_context($context); + } catch (Exception $e) { + $excludedcats[$category->id] = 'context'; - foreach ($backupsettings as $name => $value) { - $setting = $rc->get_plan()->get_setting($name); - if ($setting->get_status() == backup_setting::NOT_LOCKED) { - $setting->set_value($value); + // If it was the requested category then throw an exception. + if (isset($params['categoryid']) && $category->id == $params['categoryid']) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->catid = $category->id; + throw new moodle_exception('errorcatcontextnotvalid', 'webservice', '', $exceptionparam); + } } - } - if (!$rc->execute_precheck()) { - $precheckresults = $rc->get_precheck_results(); - if (is_array($precheckresults) && !empty($precheckresults['errors'])) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($backupbasepath); - } + // Return the category information. + if (!isset($excludedcats[$category->id])) { - $errorinfo = ''; + // Final check to see if the category is visible to the user. + if ($category->visible + or has_capability('moodle/category:viewhiddencategories', context_system::instance()) + or has_capability('moodle/category:manage', $context)) { - foreach ($precheckresults['errors'] as $error) { - $errorinfo .= $error; - } + $categoryinfo = array(); + $categoryinfo['id'] = $category->id; + $categoryinfo['name'] = $category->name; + $categoryinfo['description'] = file_rewrite_pluginfile_urls($category->description, + 'webservice/pluginfile.php', $context->id, 'coursecat', 'description', null); + $options = new stdClass; + $options->noclean = true; + $options->para = false; + $categoryinfo['description'] = format_text($categoryinfo['description'], + $category->descriptionformat, $options); + $categoryinfo['parent'] = $category->parent; + $categoryinfo['sortorder'] = $category->sortorder; + $categoryinfo['coursecount'] = $category->coursecount; + $categoryinfo['depth'] = $category->depth; + $categoryinfo['path'] = $category->path; - if (array_key_exists('warnings', $precheckresults)) { - foreach ($precheckresults['warnings'] as $warning) { - $errorinfo .= $warning; + // Some fields only returned for admin. + if (has_capability('moodle/category:manage', $context)) { + $categoryinfo['idnumber'] = $category->idnumber; + $categoryinfo['visible'] = $category->visible; + $categoryinfo['visibleold'] = $category->visibleold; + $categoryinfo['timemodified'] = $category->timemodified; + $categoryinfo['theme'] = $category->theme; } - } - throw new moodle_exception('backupprecheckerrors', 'webservice', '', $errorinfo); + $categoriesinfo[] = $categoryinfo; + } else { + $excludedcats[$category->id] = 'visibility'; + } } } - $rc->execute_plan(); - $rc->destroy(); - - $course = $DB->get_record('course', array('id' => $newcourseid), '*', MUST_EXIST); - $course->fullname = $params['fullname']; - $course->shortname = $params['shortname']; - $course->visible = $params['visible']; - - // Set shortname and fullname back. - $DB->update_record('course', $course); + // Sorting the resulting array so it looks a bit better for the client developer. + usort($categoriesinfo, "core_course_external::compare_categories_by_sortorder"); - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($backupbasepath); - } + return $categoriesinfo; + } - // Delete the course backup file created by this WebService. Originally located in the course backups area. - $file->delete(); + /** + * Sort categories array by path + * private function: only used by get_categories + * + * @param array $category1 + * @param array $category2 + * @return int result of strcmp + * @since Moodle 2.3 + */ + private static function compare_categories_by_path($category1, $category2) { + return strcmp($category1->path, $category2->path); + } - return array('id' => $course->id, 'shortname' => $course->shortname); + /** + * Sort categories array by sortorder + * private function: only used by get_categories + * + * @param array $category1 + * @param array $category2 + * @return int result of strcmp + * @since Moodle 2.3 + */ + private static function compare_categories_by_sortorder($category1, $category2) { + return strcmp($category1['sortorder'], $category2['sortorder']); } /** @@ -1280,31 +1156,50 @@ class core_course_external extends external_api { * @return external_description * @since Moodle 2.3 */ - public static function duplicate_course_returns() { - return new external_single_structure( - array( - 'id' => new external_value(PARAM_INT, 'course id'), - 'shortname' => new external_value(PARAM_TEXT, 'short name'), + public static function get_categories_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'category id'), + 'name' => new external_value(PARAM_TEXT, 'category name'), + 'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL), + 'description' => new external_value(PARAM_RAW, 'category description'), + 'parent' => new external_value(PARAM_INT, 'parent category id'), + 'sortorder' => new external_value(PARAM_INT, 'category sorting order'), + 'coursecount' => new external_value(PARAM_INT, 'number of courses in this category'), + 'visible' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), + 'visibleold' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), + 'timemodified' => new external_value(PARAM_INT, 'timestamp', VALUE_OPTIONAL), + 'depth' => new external_value(PARAM_INT, 'category depth'), + 'path' => new external_value(PARAM_TEXT, 'category path'), + 'theme' => new external_value(PARAM_THEME, 'category theme', VALUE_OPTIONAL), + ), 'List of categories' ) ); } /** * Returns description of method parameters + * * @return external_function_parameters * @since Moodle 2.3 */ - public static function delete_categories_parameters() { + public static function create_categories_parameters() { return new external_function_parameters( array( 'categories' => new external_multiple_structure( - new external_single_structure( - array( - 'id' => new external_value(PARAM_INT, 'category id to delete'), - 'newparent' => new external_value(PARAM_INT, - 'the parent category to move the contents to, if specified', VALUE_OPTIONAL), - 'recursive' => new external_value(PARAM_BOOL, '1: recursively delete all contents inside this - category, 0 (default): move contents to newparent or current parent category (except if parent is root)', VALUE_DEFAULT, 0) + new external_single_structure( + array( + 'name' => new external_value(PARAM_TEXT, 'new category name'), + 'parent' => new external_value(PARAM_INT, + 'the parent category id inside which the new category will be created'), + 'idnumber' => new external_value(PARAM_RAW, + 'the new category idnumber', VALUE_OPTIONAL), + 'description' => new external_value(PARAM_RAW, + 'the new category description', VALUE_OPTIONAL), + 'theme' => new external_value(PARAM_THEME, + 'the new category theme. This option must be enabled on moodle', + VALUE_OPTIONAL), ) ) ) @@ -1313,68 +1208,96 @@ class core_course_external extends external_api { } /** - * Delete categories - * @param array $categories A list of category ids - * @return array + * Create categories + * + * @param array $categories - see create_categories_parameters() for the array structure + * @return array - see create_categories_returns() for the array structure * @since Moodle 2.3 */ - public static function delete_categories($categories) { + public static function create_categories($categories) { global $CFG, $DB; require_once($CFG->dirroot . "/course/lib.php"); - // Validate parameters. - $params = self::validate_parameters(self::delete_categories_parameters(), array('categories' => $categories)); + $params = self::validate_parameters(self::create_categories_parameters(), + array('categories' => $categories)); + $transaction = $DB->start_delegated_transaction(); + + $createdcategories = array(); foreach ($params['categories'] as $category) { - if (!$deletecat = $DB->get_record('course_categories', array('id' => $category['id']))) { - throw new moodle_exception('unknowcategory'); + if ($category['parent']) { + if (!$DB->record_exists('course_categories', array('id' => $category['parent']))) { + throw new moodle_exception('unknowcategory'); + } + $context = context_coursecat::instance($category['parent']); + } else { + $context = context_system::instance(); } - $context = context_coursecat::instance($deletecat->id); - require_capability('moodle/category:manage', $context); self::validate_context($context); - self::validate_context(get_category_or_system_context($deletecat->parent)); + require_capability('moodle/category:manage', $context); - if ($category['recursive']) { - // If recursive was specified, then we recursively delete the category's contents. - category_delete_full($deletecat, false); - } else { - // In this situation, we don't delete the category's contents, we either move it to newparent or parent. - // If the parent is the root, moving is not supported (because a course must always be inside a category). - // We must move to an existing category. - if (!empty($category['newparent'])) { - if (!$DB->record_exists('course_categories', array('id' => $category['newparent']))) { - throw new moodle_exception('unknowcategory'); - } - $newparent = $category['newparent']; - } else { - $newparent = $deletecat->parent; + // Check id number. + if (!empty($category['idnumber'])) { // Same as in course/editcategory_form.php . + if (textlib::strlen($category['idnumber'])>100) { + throw new moodle_exception('idnumbertoolong'); } - - // This operation is not allowed. We must move contents to an existing category. - if ($newparent == 0) { - throw new moodle_exception('movecatcontentstoroot'); + if ($existing = $DB->get_record('course_categories', array('idnumber' => $category['idnumber']))) { + if ($existing->id) { + throw new moodle_exception('idnumbertaken'); + } } + } + // Check name. + if (textlib::strlen($category['name'])>255) { + throw new moodle_exception('categorytoolong'); + } - $parentcontext = get_category_or_system_context($newparent); - require_capability('moodle/category:manage', $parentcontext); - self::validate_context($parentcontext); - category_delete_move($deletecat, $newparent, false); + $newcategory = new stdClass(); + $newcategory->name = $category['name']; + $newcategory->parent = $category['parent']; + $newcategory->idnumber = $category['idnumber']; + $newcategory->sortorder = 999; // Same as in the course/editcategory.php . + // Format the description. + if (!empty($category['description'])) { + $newcategory->description = $category['description']; + } + $newcategory->descriptionformat = FORMAT_HTML; + if (isset($category['theme']) and !empty($CFG->allowcategorythemes)) { + $newcategory->theme = $category['theme']; } + + $newcategory = create_course_category($newcategory); + // Populate special fields. + fix_course_sortorder(); + + $createdcategories[] = array('id' => $newcategory->id, 'name' => $newcategory->name); } + $transaction->allow_commit(); + + return $createdcategories; } /** * Returns description of method parameters + * * @return external_function_parameters * @since Moodle 2.3 */ - public static function delete_categories_returns() { - return null; + public static function create_categories_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'new category id'), + 'name' => new external_value(PARAM_TEXT, 'new category name'), + ) + ) + ); } /** * Returns description of method parameters + * * @return external_function_parameters * @since Moodle 2.3 */ @@ -1400,6 +1323,7 @@ class core_course_external extends external_api { /** * Update categories + * * @param array $categories The list of categories to update * @return null * @since Moodle 2.3 @@ -1461,11 +1385,101 @@ class core_course_external extends external_api { /** * Returns description of method result value + * * @return external_description + * @since Moodle 2.3 */ public static function update_categories_returns() { return null; } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function delete_categories_parameters() { + return new external_function_parameters( + array( + 'categories' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'category id to delete'), + 'newparent' => new external_value(PARAM_INT, + 'the parent category to move the contents to, if specified', VALUE_OPTIONAL), + 'recursive' => new external_value(PARAM_BOOL, '1: recursively delete all contents inside this + category, 0 (default): move contents to newparent or current parent category (except if parent is root)', VALUE_DEFAULT, 0) + ) + ) + ) + ) + ); + } + + /** + * Delete categories + * + * @param array $categories A list of category ids + * @return array + * @since Moodle 2.3 + */ + public static function delete_categories($categories) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + // Validate parameters. + $params = self::validate_parameters(self::delete_categories_parameters(), array('categories' => $categories)); + + foreach ($params['categories'] as $category) { + if (!$deletecat = $DB->get_record('course_categories', array('id' => $category['id']))) { + throw new moodle_exception('unknowcategory'); + } + $context = context_coursecat::instance($deletecat->id); + require_capability('moodle/category:manage', $context); + self::validate_context($context); + self::validate_context(get_category_or_system_context($deletecat->parent)); + + if ($category['recursive']) { + // If recursive was specified, then we recursively delete the category's contents. + category_delete_full($deletecat, false); + } else { + // In this situation, we don't delete the category's contents, we either move it to newparent or parent. + // If the parent is the root, moving is not supported (because a course must always be inside a category). + // We must move to an existing category. + if (!empty($category['newparent'])) { + if (!$DB->record_exists('course_categories', array('id' => $category['newparent']))) { + throw new moodle_exception('unknowcategory'); + } + $newparent = $category['newparent']; + } else { + $newparent = $deletecat->parent; + } + + // This operation is not allowed. We must move contents to an existing category. + if ($newparent == 0) { + throw new moodle_exception('movecatcontentstoroot'); + } + + $parentcontext = get_category_or_system_context($newparent); + require_capability('moodle/category:manage', $parentcontext); + self::validate_context($parentcontext); + category_delete_move($deletecat, $newparent, false); + } + } + + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function delete_categories_returns() { + return null; + } + } /** diff --git a/lib/db/services.php b/lib/db/services.php index da632205898..4df607ad609 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -433,22 +433,13 @@ $functions = array( // === course related functions === - 'core_course_get_categories' => array( + 'core_course_get_contents' => array( 'classname' => 'core_course_external', - 'methodname' => 'get_categories', + 'methodname' => 'get_course_contents', 'classpath' => 'course/externallib.php', - 'description' => 'Return category details', + 'description' => 'Get course contents', 'type' => 'read', - 'capabilities'=> 'moodle/category:viewhiddencategories', - ), - - 'core_course_update_categories' => array( - 'classname' => 'core_course_external', - 'methodname' => 'update_categories', - 'classpath' => 'course/externallib.php', - 'description' => 'Update categories', - 'type' => 'write', - 'capabilities'=> 'moodle:category/manage', + 'capabilities'=> 'moodle/course:update,moodle/course:viewhiddencourses', ), 'moodle_course_get_courses' => array( @@ -487,15 +478,6 @@ $functions = array( 'capabilities'=> 'moodle/course:create,moodle/course:visibility', ), - 'core_course_get_contents' => array( - 'classname' => 'core_course_external', - 'methodname' => 'get_course_contents', - 'classpath' => 'course/externallib.php', - 'description' => 'Get course contents', - 'type' => 'read', - 'capabilities'=> 'moodle/course:update,moodle/course:viewhiddencourses', - ), - 'core_course_delete_courses' => array( 'classname' => 'core_course_external', 'methodname' => 'delete_courses', @@ -514,6 +496,17 @@ $functions = array( 'capabilities'=> 'moodle/backup:backupcourse,moodle/restore:restorecourse,moodle/course:create', ), + // === course category related functions === + + 'core_course_get_categories' => array( + 'classname' => 'core_course_external', + 'methodname' => 'get_categories', + 'classpath' => 'course/externallib.php', + 'description' => 'Return category details', + 'type' => 'read', + 'capabilities'=> 'moodle/category:viewhiddencategories', + ), + 'core_course_create_categories' => array( 'classname' => 'core_course_external', 'methodname' => 'create_categories', @@ -523,6 +516,15 @@ $functions = array( 'capabilities'=> 'moodle/category:manage', ), + 'core_course_update_categories' => array( + 'classname' => 'core_course_external', + 'methodname' => 'update_categories', + 'classpath' => 'course/externallib.php', + 'description' => 'Update categories', + 'type' => 'write', + 'capabilities'=> 'moodle:category/manage', + ), + 'core_course_delete_categories' => array( 'classname' => 'core_course_external', 'methodname' => 'delete_categories', -- 2.43.0