PageRenderTime 22ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/admin/tool/uploadcourse/classes/helper.php

http://github.com/moodle/moodle
PHP | 593 lines | 339 code | 68 blank | 186 comment | 78 complexity | 25310c0df46f22ff38b58be0a31f22c0 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  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. * File containing the helper class.
  18. *
  19. * @package tool_uploadcourse
  20. * @copyright 2013 Frédéric Massart
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. require_once($CFG->dirroot . '/cache/lib.php');
  25. require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
  26. require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  27. /**
  28. * Class containing a set of helpers.
  29. *
  30. * @package tool_uploadcourse
  31. * @copyright 2013 Frédéric Massart
  32. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33. */
  34. class tool_uploadcourse_helper {
  35. /**
  36. * Generate a shortname based on a template.
  37. *
  38. * @param array|object $data course data.
  39. * @param string $templateshortname template of shortname.
  40. * @return null|string shortname based on the template, or null when an error occured.
  41. */
  42. public static function generate_shortname($data, $templateshortname) {
  43. if (empty($templateshortname) && !is_numeric($templateshortname)) {
  44. return null;
  45. }
  46. if (strpos($templateshortname, '%') === false) {
  47. return $templateshortname;
  48. }
  49. $course = (object) $data;
  50. $fullname = isset($course->fullname) ? $course->fullname : '';
  51. $idnumber = isset($course->idnumber) ? $course->idnumber : '';
  52. $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
  53. $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
  54. if (!is_null($result)) {
  55. $result = clean_param($result, PARAM_TEXT);
  56. }
  57. if (empty($result) && !is_numeric($result)) {
  58. $result = null;
  59. }
  60. return $result;
  61. }
  62. /**
  63. * Callback used when generating a shortname based on a template.
  64. *
  65. * @param string $fullname full name.
  66. * @param string $idnumber ID number.
  67. * @param array $block result from preg_replace_callback.
  68. * @return string
  69. */
  70. public static function generate_shortname_callback($fullname, $idnumber, $block) {
  71. switch ($block[3]) {
  72. case 'f':
  73. $repl = $fullname;
  74. break;
  75. case 'i':
  76. $repl = $idnumber;
  77. break;
  78. default:
  79. return $block[0];
  80. }
  81. switch ($block[1]) {
  82. case '+':
  83. $repl = core_text::strtoupper($repl);
  84. break;
  85. case '-':
  86. $repl = core_text::strtolower($repl);
  87. break;
  88. case '~':
  89. $repl = core_text::strtotitle($repl);
  90. break;
  91. }
  92. if (!empty($block[2])) {
  93. $repl = core_text::substr($repl, 0, $block[2]);
  94. }
  95. return $repl;
  96. }
  97. /**
  98. * Return the available course formats.
  99. *
  100. * @return array
  101. */
  102. public static function get_course_formats() {
  103. return array_keys(core_component::get_plugin_list('format'));
  104. }
  105. /**
  106. * Extract enrolment data from passed data.
  107. *
  108. * Constructs an array of methods, and their options:
  109. * array(
  110. * 'method1' => array(
  111. * 'option1' => value,
  112. * 'option2' => value
  113. * ),
  114. * 'method2' => array(
  115. * 'option1' => value,
  116. * 'option2' => value
  117. * )
  118. * )
  119. *
  120. * @param array $data data to extract the enrolment data from.
  121. * @return array
  122. */
  123. public static function get_enrolment_data($data) {
  124. $enrolmethods = array();
  125. $enroloptions = array();
  126. foreach ($data as $field => $value) {
  127. // Enrolmnent data.
  128. $matches = array();
  129. if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) {
  130. $key = $matches[1];
  131. if (!isset($enroloptions[$key])) {
  132. $enroloptions[$key] = array();
  133. }
  134. if (empty($matches[3])) {
  135. $enrolmethods[$key] = $value;
  136. } else {
  137. $enroloptions[$key][$matches[3]] = $value;
  138. }
  139. }
  140. }
  141. // Combining enrolment methods and their options in a single array.
  142. $enrolmentdata = array();
  143. if (!empty($enrolmethods)) {
  144. $enrolmentplugins = self::get_enrolment_plugins();
  145. foreach ($enrolmethods as $key => $method) {
  146. if (!array_key_exists($method, $enrolmentplugins)) {
  147. // Error!
  148. continue;
  149. }
  150. $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
  151. }
  152. }
  153. return $enrolmentdata;
  154. }
  155. /**
  156. * Return the enrolment plugins.
  157. *
  158. * The result is cached for faster execution.
  159. *
  160. * @return array
  161. */
  162. public static function get_enrolment_plugins() {
  163. $cache = cache::make('tool_uploadcourse', 'helper');
  164. if (($enrol = $cache->get('enrol')) === false) {
  165. $enrol = enrol_get_plugins(false);
  166. $cache->set('enrol', $enrol);
  167. }
  168. return $enrol;
  169. }
  170. /**
  171. * Get the restore content tempdir.
  172. *
  173. * The tempdir is the sub directory in which the backup has been extracted.
  174. *
  175. * This caches the result for better performance, but $CFG->keeptempdirectoriesonbackup
  176. * needs to be enabled, otherwise the cache is ignored.
  177. *
  178. * @param string $backupfile path to a backup file.
  179. * @param string $shortname shortname of a course.
  180. * @param array $errors will be populated with errors found.
  181. * @return string|false false when the backup couldn't retrieved.
  182. */
  183. public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
  184. global $CFG, $DB, $USER;
  185. $cachekey = null;
  186. if (!empty($backupfile)) {
  187. $backupfile = realpath($backupfile);
  188. if (empty($backupfile) || !is_readable($backupfile)) {
  189. $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
  190. return false;
  191. }
  192. $cachekey = 'backup_path:' . $backupfile;
  193. } else if (!empty($shortname) || is_numeric($shortname)) {
  194. $cachekey = 'backup_sn:' . $shortname;
  195. }
  196. if (empty($cachekey)) {
  197. return false;
  198. }
  199. // If $CFG->keeptempdirectoriesonbackup is not set to true, any restore happening would
  200. // automatically delete the backup directory... causing the cache to return an unexisting directory.
  201. $usecache = !empty($CFG->keeptempdirectoriesonbackup);
  202. if ($usecache) {
  203. $cache = cache::make('tool_uploadcourse', 'helper');
  204. }
  205. // If we don't use the cache, or if we do and not set, or the directory doesn't exist any more.
  206. if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir(get_backup_temp_directory($backupid)))) {
  207. // Use null instead of false because it would consider that the cache key has not been set.
  208. $backupid = null;
  209. if (!empty($backupfile)) {
  210. // Extracting the backup file.
  211. $packer = get_file_packer('application/vnd.moodle.backup');
  212. $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
  213. $path = make_backup_temp_directory($backupid, false);
  214. $result = $packer->extract_to_pathname($backupfile, $path);
  215. if (!$result) {
  216. $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
  217. }
  218. } else if (!empty($shortname) || is_numeric($shortname)) {
  219. // Creating restore from an existing course.
  220. $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
  221. if (!empty($courseid)) {
  222. $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
  223. backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
  224. $bc->execute_plan();
  225. $backupid = $bc->get_backupid();
  226. $bc->destroy();
  227. } else {
  228. $errors['coursetorestorefromdoesnotexist'] =
  229. new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
  230. }
  231. }
  232. if ($usecache) {
  233. $cache->set($cachekey, $backupid);
  234. }
  235. }
  236. if ($backupid === null) {
  237. $backupid = false;
  238. }
  239. return $backupid;
  240. }
  241. /**
  242. * Return the role IDs.
  243. *
  244. * The result is cached for faster execution.
  245. *
  246. * @return array
  247. */
  248. public static function get_role_ids() {
  249. $cache = cache::make('tool_uploadcourse', 'helper');
  250. if (($roles = $cache->get('roles')) === false) {
  251. $roles = array();
  252. $rolesraw = get_all_roles();
  253. foreach ($rolesraw as $role) {
  254. $roles[$role->shortname] = $role->id;
  255. }
  256. $cache->set('roles', $roles);
  257. }
  258. return $roles;
  259. }
  260. /**
  261. * Helper to detect how many sections a course with a given shortname has.
  262. *
  263. * @param string $shortname shortname of a course to count sections from.
  264. * @return integer count of sections.
  265. */
  266. public static function get_coursesection_count($shortname) {
  267. global $DB;
  268. if (!empty($shortname) || is_numeric($shortname)) {
  269. // Creating restore from an existing course.
  270. $course = $DB->get_record('course', array('shortname' => $shortname));
  271. }
  272. if (!empty($course)) {
  273. $courseformat = course_get_format($course);
  274. return $courseformat->get_last_section_number();
  275. }
  276. return 0;
  277. }
  278. /**
  279. * Get the role renaming data from the passed data.
  280. *
  281. * @param array $data data to extract the names from.
  282. * @param array $errors will be populated with errors found.
  283. * @return array where the key is the role_<id>, the value is the new name.
  284. */
  285. public static function get_role_names($data, &$errors = array()) {
  286. $rolenames = array();
  287. $rolesids = self::get_role_ids();
  288. $invalidroles = array();
  289. foreach ($data as $field => $value) {
  290. $matches = array();
  291. if (preg_match('/^role_(.+)?$/', $field, $matches)) {
  292. if (!isset($rolesids[$matches[1]])) {
  293. $invalidroles[] = $matches[1];
  294. continue;
  295. }
  296. $rolenames['role_' . $rolesids[$matches[1]]] = $value;
  297. }
  298. }
  299. if (!empty($invalidroles)) {
  300. $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
  301. }
  302. // Roles names.
  303. return $rolenames;
  304. }
  305. /**
  306. * Return array of all custom course fields indexed by their shortname
  307. *
  308. * @return \core_customfield\field_controller[]
  309. */
  310. public static function get_custom_course_fields(): array {
  311. $result = [];
  312. $fields = \core_course\customfield\course_handler::create()->get_fields();
  313. foreach ($fields as $field) {
  314. $result[$field->get('shortname')] = $field;
  315. }
  316. return $result;
  317. }
  318. /**
  319. * Return array of custom field element names
  320. *
  321. * @return string[]
  322. */
  323. public static function get_custom_course_field_names(): array {
  324. $result = [];
  325. $fields = self::get_custom_course_fields();
  326. foreach ($fields as $field) {
  327. $controller = \core_customfield\data_controller::create(0, null, $field);
  328. $result[] = $controller->get_form_element_name();
  329. }
  330. return $result;
  331. }
  332. /**
  333. * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
  334. *
  335. * @param array $data
  336. * @param array $defaults
  337. * @param context $context
  338. * @param array $errors Will be populated with any errors
  339. * @return array
  340. */
  341. public static function get_custom_course_field_data(array $data, array $defaults, context $context,
  342. array &$errors = []): array {
  343. $fields = self::get_custom_course_fields();
  344. $result = [];
  345. $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
  346. foreach ($data as $name => $originalvalue) {
  347. if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
  348. && isset($fields[$matches['name']])) {
  349. $fieldname = $matches['name'];
  350. $field = $fields[$fieldname];
  351. // Skip field if it's locked and user doesn't have capability to change locked fields.
  352. if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
  353. continue;
  354. }
  355. // Create field data controller.
  356. $controller = \core_customfield\data_controller::create(0, null, $field);
  357. $controller->set('id', 1);
  358. $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
  359. $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
  360. // If we initially had a value, but now don't, then reset it to the default.
  361. if (!empty($originalvalue) && empty($value)) {
  362. $value = $defaultvalue;
  363. }
  364. // Validate data with controller.
  365. $fieldformdata = [$controller->get_form_element_name() => $value];
  366. $validationerrors = $controller->instance_form_validation($fieldformdata, []);
  367. if (count($validationerrors) > 0) {
  368. $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
  369. $field->get_formatted_name());
  370. continue;
  371. }
  372. $controller->set($controller->datafield(), $value);
  373. // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
  374. $instance = new stdClass();
  375. $controller->instance_form_before_set_data($instance);
  376. $result = array_merge($result, (array) $instance);
  377. }
  378. }
  379. return $result;
  380. }
  381. /**
  382. * Helper to increment an ID number.
  383. *
  384. * This first checks if the ID number is in use.
  385. *
  386. * @param string $idnumber ID number to increment.
  387. * @return string new ID number.
  388. */
  389. public static function increment_idnumber($idnumber) {
  390. global $DB;
  391. while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
  392. $matches = array();
  393. if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
  394. $newidnumber = $idnumber . '_2';
  395. } else {
  396. $newidnumber = $matches[1] . ((int) $matches[2] + 1);
  397. }
  398. $idnumber = $newidnumber;
  399. }
  400. return $idnumber;
  401. }
  402. /**
  403. * Helper to increment a shortname.
  404. *
  405. * This considers that the shortname passed has to be incremented.
  406. *
  407. * @param string $shortname shortname to increment.
  408. * @return string new shortname.
  409. */
  410. public static function increment_shortname($shortname) {
  411. global $DB;
  412. do {
  413. $matches = array();
  414. if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
  415. $newshortname = $shortname . '_2';
  416. } else {
  417. $newshortname = $matches[1] . ($matches[2]+1);
  418. }
  419. $shortname = $newshortname;
  420. } while ($DB->record_exists('course', array('shortname' => $shortname)));
  421. return $shortname;
  422. }
  423. /**
  424. * Resolve a category based on the data passed.
  425. *
  426. * Key accepted are:
  427. * - category, which is supposed to be a category ID.
  428. * - category_idnumber
  429. * - category_path, array of categories from parent to child.
  430. *
  431. * @param array $data to resolve the category from.
  432. * @param array $errors will be populated with errors found.
  433. * @return int category ID.
  434. */
  435. public static function resolve_category($data, &$errors = array()) {
  436. $catid = null;
  437. if (!empty($data['category'])) {
  438. $category = core_course_category::get((int) $data['category'], IGNORE_MISSING);
  439. if (!empty($category) && !empty($category->id)) {
  440. $catid = $category->id;
  441. } else {
  442. $errors['couldnotresolvecatgorybyid'] =
  443. new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
  444. }
  445. }
  446. if (empty($catid) && !empty($data['category_idnumber'])) {
  447. $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
  448. if (empty($catid)) {
  449. $errors['couldnotresolvecatgorybyidnumber'] =
  450. new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
  451. }
  452. }
  453. if (empty($catid) && !empty($data['category_path'])) {
  454. $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
  455. if (empty($catid)) {
  456. $errors['couldnotresolvecatgorybypath'] =
  457. new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
  458. }
  459. }
  460. return $catid;
  461. }
  462. /**
  463. * Resolve a category by ID number.
  464. *
  465. * @param string $idnumber category ID number.
  466. * @return int category ID.
  467. */
  468. public static function resolve_category_by_idnumber($idnumber) {
  469. global $DB;
  470. $cache = cache::make('tool_uploadcourse', 'helper');
  471. $cachekey = 'cat_idn_' . $idnumber;
  472. if (($id = $cache->get($cachekey)) === false) {
  473. $params = array('idnumber' => $idnumber);
  474. $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
  475. // Little hack to be able to differenciate between the cache not set and a category not found.
  476. if ($id === false) {
  477. $id = -1;
  478. }
  479. $cache->set($cachekey, $id);
  480. }
  481. // Little hack to be able to differenciate between the cache not set and a category not found.
  482. if ($id == -1) {
  483. $id = false;
  484. }
  485. return $id;
  486. }
  487. /**
  488. * Resolve a category by path.
  489. *
  490. * @param array $path category names indexed from parent to children.
  491. * @return int category ID.
  492. */
  493. public static function resolve_category_by_path(array $path) {
  494. global $DB;
  495. $cache = cache::make('tool_uploadcourse', 'helper');
  496. $cachekey = 'cat_path_' . serialize($path);
  497. if (($id = $cache->get($cachekey)) === false) {
  498. $parent = 0;
  499. $sql = 'name = :name AND parent = :parent';
  500. while ($name = array_shift($path)) {
  501. $params = array('name' => $name, 'parent' => $parent);
  502. if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
  503. if (count($records) > 1) {
  504. // Too many records with the same name!
  505. $id = -1;
  506. break;
  507. }
  508. $record = reset($records);
  509. $id = $record->id;
  510. $parent = $record->id;
  511. } else {
  512. // Not found.
  513. $id = -1;
  514. break;
  515. }
  516. }
  517. $cache->set($cachekey, $id);
  518. }
  519. // We save -1 when the category has not been found to be able to know if the cache was set.
  520. if ($id == -1) {
  521. $id = false;
  522. }
  523. return $id;
  524. }
  525. }