Revert "Merge branch 'MDL-43127-master-fixup' of https://github.com/FMCorz/moodle"
[moodle.git] / admin / tool / uploadcourse / classes / helper.php
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/>.
17 /**
18  * File containing the helper class.
19  *
20  * @package    tool_uploadcourse
21  * @copyright  2013 Frédéric Massart
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
26 require_once($CFG->libdir . '/coursecatlib.php');
27 require_once($CFG->dirroot . '/cache/lib.php');
28 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
29 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
31 /**
32  * Class containing a set of helpers.
33  *
34  * @package    tool_uploadcourse
35  * @copyright  2013 Frédéric Massart
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class tool_uploadcourse_helper {
40     /**
41      * Generate a shortname based on a template.
42      *
43      * @param array|object $data course data.
44      * @param string $templateshortname template of shortname.
45      * @return null|string shortname based on the template, or null when an error occured.
46      */
47     public static function generate_shortname($data, $templateshortname) {
48         if (empty($templateshortname) && !is_numeric($templateshortname)) {
49             return null;
50         }
51         if (strpos($templateshortname, '%') === false) {
52             return $templateshortname;
53         }
55         $course = (object) $data;
56         $fullname   = isset($course->fullname) ? $course->fullname : '';
57         $idnumber   = isset($course->idnumber) ? $course->idnumber  : '';
59         $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
60         $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
62         if (!is_null($result)) {
63             $result = clean_param($result, PARAM_TEXT);
64         }
66         if (empty($result) && !is_numeric($result)) {
67             $result = null;
68         }
70         return $result;
71     }
73     /**
74      * Callback used when generating a shortname based on a template.
75      *
76      * @param string $fullname full name.
77      * @param string $idnumber ID number.
78      * @param array $block result from preg_replace_callback.
79      * @return string
80      */
81     public static function generate_shortname_callback($fullname, $idnumber, $block) {
82         switch ($block[3]) {
83             case 'f':
84                 $repl = $fullname;
85                 break;
86             case 'i':
87                 $repl = $idnumber;
88                 break;
89             default:
90                 return $block[0];
91         }
93         switch ($block[1]) {
94             case '+':
95                 $repl = core_text::strtoupper($repl);
96                 break;
97             case '-':
98                 $repl = core_text::strtolower($repl);
99                 break;
100             case '~':
101                 $repl = core_text::strtotitle($repl);
102                 break;
103         }
105         if (!empty($block[2])) {
106             $repl = core_text::substr($repl, 0, $block[2]);
107         }
109         return $repl;
110     }
112     /**
113      * Return the available course formats.
114      *
115      * @return array
116      */
117     public static function get_course_formats() {
118         return array_keys(core_component::get_plugin_list('format'));
119     }
121     /**
122      * Extract enrolment data from passed data.
123      *
124      * Constructs an array of methods, and their options:
125      * array(
126      *     'method1' => array(
127      *         'option1' => value,
128      *         'option2' => value
129      *     ),
130      *     'method2' => array(
131      *         'option1' => value,
132      *         'option2' => value
133      *     )
134      * )
135      *
136      * @param array $data data to extract the enrolment data from.
137      * @param array $errors will be populated with errors found.
138      * @return array
139      */
140     public static function get_enrolment_data($data, &$errors = array()) {
141         $enrolmethods = array();
142         $enroloptions = array();
143         foreach ($data as $field => $value) {
145             // Enrolmnent data.
146             $matches = array();
147             if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) {
148                 $key = $matches[1];
149                 if (!isset($enroloptions[$key])) {
150                     $enroloptions[$key] = array();
151                 }
152                 if (empty($matches[3])) {
153                     $enrolmethods[$key] = $value;
154                 } else {
155                     $enroloptions[$key][$matches[3]] = $value;
156                 }
157             }
158         }
160         // Combining enrolment methods and their options in a single array.
161         $enrolmentdata = array();
162         $unknownmethods = array();
163         $methodsnotsupported = array();
164         if (!empty($enrolmethods)) {
165             $enrolmentplugins = self::get_enrolment_plugins();
166             foreach ($enrolmethods as $key => $method) {
167                 if (!array_key_exists($method, $enrolmentplugins)) {
168                     // Unknown enrolment method.
169                     $unknownmethods[] = $method;
170                     continue;
171                 }
172                 $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
173             }
174         }
176         // Logging errors related to enrolment methods.
177         if (!empty($unknownmethods)) {
178             $errors['unknownenrolmentmethods'] = new lang_string('unknownenrolmentmethods', 'tool_uploadcourse',
179                 implode(', ', $unknownmethods));
180         }
182         return $enrolmentdata;
183     }
185     /**
186      * Return the enrolment plugins.
187      *
188      * The result is cached for faster execution.
189      *
190      * @return array
191      */
192     public static function get_enrolment_plugins() {
193         $cache = cache::make('tool_uploadcourse', 'helper');
194         if (($enrol = $cache->get('enrol')) === false) {
195             $enrol = enrol_get_plugins(false);
196             $cache->set('enrol', $enrol);
197         }
198         return $enrol;
199     }
201     /**
202      * Get the restore content tempdir.
203      *
204      * The tempdir is the sub directory in which the backup has been extracted.
205      *
206      * This caches the result for better performance, but $CFG->keeptempdirectoriesonbackup
207      * needs to be enabled, otherwise the cache is ignored.
208      *
209      * @param string $backupfile path to a backup file.
210      * @param string $shortname shortname of a course.
211      * @param array $errors will be populated with errors found.
212      * @return string|false false when the backup couldn't retrieved.
213      */
214     public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
215         global $CFG, $DB, $USER;
217         $cachekey = null;
218         if (!empty($backupfile)) {
219             $backupfile = realpath($backupfile);
220             if (empty($backupfile) || !is_readable($backupfile)) {
221                 $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
222                 return false;
223             }
224             $cachekey = 'backup_path:' . $backupfile;
225         } else if (!empty($shortname) || is_numeric($shortname)) {
226             $cachekey = 'backup_sn:' . $shortname;
227         }
229         if (empty($cachekey)) {
230             return false;
231         }
233         // If $CFG->keeptempdirectoriesonbackup is not set to true, any restore happening would
234         // automatically delete the backup directory... causing the cache to return an unexisting directory.
235         $usecache = !empty($CFG->keeptempdirectoriesonbackup);
236         if ($usecache) {
237             $cache = cache::make('tool_uploadcourse', 'helper');
238         }
240         // If we don't use the cache, or if we do and not set, or the directory doesn't exist any more.
241         if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir("$CFG->tempdir/backup/$backupid"))) {
243             // Use null instead of false because it would consider that the cache key has not been set.
244             $backupid = null;
246             if (!empty($backupfile)) {
247                 // Extracting the backup file.
248                 $packer = get_file_packer('application/vnd.moodle.backup');
249                 $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
250                 $path = "$CFG->tempdir/backup/$backupid/";
251                 $result = $packer->extract_to_pathname($backupfile, $path);
252                 if (!$result) {
253                     $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
254                 }
255             } else if (!empty($shortname) || is_numeric($shortname)) {
256                 // Creating restore from an existing course.
257                 $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
258                 if (!empty($courseid)) {
259                     $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
260                         backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
261                     $bc->execute_plan();
262                     $backupid = $bc->get_backupid();
263                     $bc->destroy();
264                 } else {
265                     $errors['coursetorestorefromdoesnotexist'] =
266                         new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
267                 }
268             }
270             if ($usecache) {
271                 $cache->set($cachekey, $backupid);
272             }
273         }
275         if ($backupid === null) {
276             $backupid = false;
277         }
278         return $backupid;
279     }
281     /**
282      * Return the role IDs.
283      *
284      * The result is cached for faster execution.
285      *
286      * @return array
287      */
288     public static function get_role_ids() {
289         $cache = cache::make('tool_uploadcourse', 'helper');
290         if (($roles = $cache->get('roles')) === false) {
291             $roles = array();
292             $rolesraw = get_all_roles();
293             foreach ($rolesraw as $role) {
294                 $roles[$role->shortname] = $role->id;
295             }
296             $cache->set('roles', $roles);
297         }
298         return $roles;
299     }
301     /**
302      * Get the role renaming data from the passed data.
303      *
304      * @param array $data data to extract the names from.
305      * @param array $errors will be populated with errors found.
306      * @return array where the key is the role_<id>, the value is the new name.
307      */
308     public static function get_role_names($data, &$errors = array()) {
309         $rolenames = array();
310         $rolesids = self::get_role_ids();
311         $invalidroles = array();
312         foreach ($data as $field => $value) {
314             $matches = array();
315             if (preg_match('/^role_(.+)?$/', $field, $matches)) {
316                 if (!isset($rolesids[$matches[1]])) {
317                     $invalidroles[] = $matches[1];
318                     continue;
319                 }
320                 $rolenames['role_' . $rolesids[$matches[1]]] = $value;
321             }
323         }
325         if (!empty($invalidroles)) {
326             $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
327         }
329         // Roles names.
330         return $rolenames;
331     }
333     /**
334      * Helper to increment an ID number.
335      *
336      * This first checks if the ID number is in use.
337      *
338      * @param string $idnumber ID number to increment.
339      * @return string new ID number.
340      */
341     public static function increment_idnumber($idnumber) {
342         global $DB;
343         while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
344             $matches = array();
345             if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
346                 $newidnumber = $idnumber . '_2';
347             } else {
348                 $newidnumber = $matches[1] . ((int) $matches[2] + 1);
349             }
350             $idnumber = $newidnumber;
351         }
352         return $idnumber;
353     }
355     /**
356      * Helper to increment a shortname.
357      *
358      * This considers that the shortname passed has to be incremented.
359      *
360      * @param string $shortname shortname to increment.
361      * @return string new shortname.
362      */
363     public static function increment_shortname($shortname) {
364         global $DB;
365         do {
366             $matches = array();
367             if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
368                 $newshortname = $shortname . '_2';
369             } else {
370                 $newshortname = $matches[1] . ($matches[2]+1);
371             }
372             $shortname = $newshortname;
373         } while ($DB->record_exists('course', array('shortname' => $shortname)));
374         return $shortname;
375     }
377     /**
378      * Resolve a category based on the data passed.
379      *
380      * Key accepted are:
381      * - category, which is supposed to be a category ID.
382      * - category_idnumber
383      * - category_path, array of categories from parent to child.
384      *
385      * @param array $data to resolve the category from.
386      * @param array $errors will be populated with errors found.
387      * @return int category ID.
388      */
389     public static function resolve_category($data, &$errors = array()) {
390         $catid = null;
392         if (!empty($data['category'])) {
393             $category = coursecat::get((int) $data['category'], IGNORE_MISSING);
394             if (!empty($category) && !empty($category->id)) {
395                 $catid = $category->id;
396             } else {
397                 $errors['couldnotresolvecatgorybyid'] =
398                     new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
399             }
400         }
402         if (empty($catid) && !empty($data['category_idnumber'])) {
403             $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
404             if (empty($catid)) {
405                 $errors['couldnotresolvecatgorybyidnumber'] =
406                     new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
407             }
408         }
409         if (empty($catid) && !empty($data['category_path'])) {
410             $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
411             if (empty($catid)) {
412                 $errors['couldnotresolvecatgorybypath'] =
413                     new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
414             }
415         }
417         return $catid;
418     }
420     /**
421      * Resolve a category by ID number.
422      *
423      * @param string $idnumber category ID number.
424      * @return int category ID.
425      */
426     public static function resolve_category_by_idnumber($idnumber) {
427         global $DB;
428         $cache = cache::make('tool_uploadcourse', 'helper');
429         $cachekey = 'cat_idn_' . $idnumber;
430         if (($id = $cache->get($cachekey)) === false) {
431             $params = array('idnumber' => $idnumber);
432             $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
434             // Little hack to be able to differenciate between the cache not set and a category not found.
435             if ($id === false) {
436                 $id = -1;
437             }
439             $cache->set($cachekey, $id);
440         }
442         // Little hack to be able to differenciate between the cache not set and a category not found.
443         if ($id == -1) {
444             $id = false;
445         }
447         return $id;
448     }
450     /**
451      * Resolve a category by path.
452      *
453      * @param array $path category names indexed from parent to children.
454      * @return int category ID.
455      */
456     public static function resolve_category_by_path(array $path) {
457         global $DB;
458         $cache = cache::make('tool_uploadcourse', 'helper');
459         $cachekey = 'cat_path_' . serialize($path);
460         if (($id = $cache->get($cachekey)) === false) {
461             $parent = 0;
462             $sql = 'name = :name AND parent = :parent';
463             while ($name = array_shift($path)) {
464                 $params = array('name' => $name, 'parent' => $parent);
465                 if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
466                     if (count($records) > 1) {
467                         // Too many records with the same name!
468                         $id = -1;
469                         break;
470                     }
471                     $record = reset($records);
472                     $id = $record->id;
473                     $parent = $record->id;
474                 } else {
475                     // Not found.
476                     $id = -1;
477                     break;
478                 }
479             }
480             $cache->set($cachekey, $id);
481         }
483         // We save -1 when the category has not been found to be able to know if the cache was set.
484         if ($id == -1) {
485             $id = false;
486         }
487         return $id;
488     }