5ffc6779e3f2b40106836e5b9d6021e3a6947f48
[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 (empty($method)) {
168                     // The enrolment method is not specified, we skip it.
169                     continue;
170                 } else if (!array_key_exists($method, $enrolmentplugins)) {
171                     // Unknown enrolment method.
172                     $unknownmethods[] = $method;
173                     continue;
174                 }
175                 $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
176             }
177         }
179         // Logging errors related to enrolment methods.
180         if (!empty($unknownmethods)) {
181             $errors['unknownenrolmentmethods'] = new lang_string('unknownenrolmentmethods', 'tool_uploadcourse',
182                 implode(', ', $unknownmethods));
183         }
185         return $enrolmentdata;
186     }
188     /**
189      * Return the enrolment plugins.
190      *
191      * The result is cached for faster execution.
192      *
193      * @return array
194      */
195     public static function get_enrolment_plugins() {
196         $cache = cache::make('tool_uploadcourse', 'helper');
197         if (($enrol = $cache->get('enrol')) === false) {
198             $enrol = enrol_get_plugins(false);
199             $cache->set('enrol', $enrol);
200         }
201         return $enrol;
202     }
204     /**
205      * Get the restore content tempdir.
206      *
207      * The tempdir is the sub directory in which the backup has been extracted.
208      *
209      * This caches the result for better performance, but $CFG->keeptempdirectoriesonbackup
210      * needs to be enabled, otherwise the cache is ignored.
211      *
212      * @param string $backupfile path to a backup file.
213      * @param string $shortname shortname of a course.
214      * @param array $errors will be populated with errors found.
215      * @return string|false false when the backup couldn't retrieved.
216      */
217     public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
218         global $CFG, $DB, $USER;
220         $cachekey = null;
221         if (!empty($backupfile)) {
222             $backupfile = realpath($backupfile);
223             if (empty($backupfile) || !is_readable($backupfile)) {
224                 $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
225                 return false;
226             }
227             $cachekey = 'backup_path:' . $backupfile;
228         } else if (!empty($shortname) || is_numeric($shortname)) {
229             $cachekey = 'backup_sn:' . $shortname;
230         }
232         if (empty($cachekey)) {
233             return false;
234         }
236         // If $CFG->keeptempdirectoriesonbackup is not set to true, any restore happening would
237         // automatically delete the backup directory... causing the cache to return an unexisting directory.
238         $usecache = !empty($CFG->keeptempdirectoriesonbackup);
239         if ($usecache) {
240             $cache = cache::make('tool_uploadcourse', 'helper');
241         }
243         // If we don't use the cache, or if we do and not set, or the directory doesn't exist any more.
244         if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir("$CFG->tempdir/backup/$backupid"))) {
246             // Use null instead of false because it would consider that the cache key has not been set.
247             $backupid = null;
249             if (!empty($backupfile)) {
250                 // Extracting the backup file.
251                 $packer = get_file_packer('application/vnd.moodle.backup');
252                 $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
253                 $path = "$CFG->tempdir/backup/$backupid/";
254                 $result = $packer->extract_to_pathname($backupfile, $path);
255                 if (!$result) {
256                     $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
257                 }
258             } else if (!empty($shortname) || is_numeric($shortname)) {
259                 // Creating restore from an existing course.
260                 $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
261                 if (!empty($courseid)) {
262                     $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
263                         backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
264                     $bc->execute_plan();
265                     $backupid = $bc->get_backupid();
266                     $bc->destroy();
267                 } else {
268                     $errors['coursetorestorefromdoesnotexist'] =
269                         new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
270                 }
271             }
273             if ($usecache) {
274                 $cache->set($cachekey, $backupid);
275             }
276         }
278         if ($backupid === null) {
279             $backupid = false;
280         }
281         return $backupid;
282     }
284     /**
285      * Return the role IDs.
286      *
287      * The result is cached for faster execution.
288      *
289      * @return array
290      */
291     public static function get_role_ids() {
292         $cache = cache::make('tool_uploadcourse', 'helper');
293         if (($roles = $cache->get('roles')) === false) {
294             $roles = array();
295             $rolesraw = get_all_roles();
296             foreach ($rolesraw as $role) {
297                 $roles[$role->shortname] = $role->id;
298             }
299             $cache->set('roles', $roles);
300         }
301         return $roles;
302     }
304     /**
305      * Get the role renaming data from the passed data.
306      *
307      * @param array $data data to extract the names from.
308      * @param array $errors will be populated with errors found.
309      * @return array where the key is the role_<id>, the value is the new name.
310      */
311     public static function get_role_names($data, &$errors = array()) {
312         $rolenames = array();
313         $rolesids = self::get_role_ids();
314         $invalidroles = array();
315         foreach ($data as $field => $value) {
317             $matches = array();
318             if (preg_match('/^role_(.+)?$/', $field, $matches)) {
319                 if (!isset($rolesids[$matches[1]])) {
320                     $invalidroles[] = $matches[1];
321                     continue;
322                 }
323                 $rolenames['role_' . $rolesids[$matches[1]]] = $value;
324             }
326         }
328         if (!empty($invalidroles)) {
329             $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
330         }
332         // Roles names.
333         return $rolenames;
334     }
336     /**
337      * Helper to increment an ID number.
338      *
339      * This first checks if the ID number is in use.
340      *
341      * @param string $idnumber ID number to increment.
342      * @return string new ID number.
343      */
344     public static function increment_idnumber($idnumber) {
345         global $DB;
346         while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
347             $matches = array();
348             if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
349                 $newidnumber = $idnumber . '_2';
350             } else {
351                 $newidnumber = $matches[1] . ((int) $matches[2] + 1);
352             }
353             $idnumber = $newidnumber;
354         }
355         return $idnumber;
356     }
358     /**
359      * Helper to increment a shortname.
360      *
361      * This considers that the shortname passed has to be incremented.
362      *
363      * @param string $shortname shortname to increment.
364      * @return string new shortname.
365      */
366     public static function increment_shortname($shortname) {
367         global $DB;
368         do {
369             $matches = array();
370             if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
371                 $newshortname = $shortname . '_2';
372             } else {
373                 $newshortname = $matches[1] . ($matches[2]+1);
374             }
375             $shortname = $newshortname;
376         } while ($DB->record_exists('course', array('shortname' => $shortname)));
377         return $shortname;
378     }
380     /**
381      * Resolve a category based on the data passed.
382      *
383      * Key accepted are:
384      * - category, which is supposed to be a category ID.
385      * - category_idnumber
386      * - category_path, array of categories from parent to child.
387      *
388      * @param array $data to resolve the category from.
389      * @param array $errors will be populated with errors found.
390      * @return int category ID.
391      */
392     public static function resolve_category($data, &$errors = array()) {
393         $catid = null;
395         if (!empty($data['category'])) {
396             $category = coursecat::get((int) $data['category'], IGNORE_MISSING);
397             if (!empty($category) && !empty($category->id)) {
398                 $catid = $category->id;
399             } else {
400                 $errors['couldnotresolvecatgorybyid'] =
401                     new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
402             }
403         }
405         if (empty($catid) && !empty($data['category_idnumber'])) {
406             $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
407             if (empty($catid)) {
408                 $errors['couldnotresolvecatgorybyidnumber'] =
409                     new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
410             }
411         }
412         if (empty($catid) && !empty($data['category_path'])) {
413             $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
414             if (empty($catid)) {
415                 $errors['couldnotresolvecatgorybypath'] =
416                     new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
417             }
418         }
420         return $catid;
421     }
423     /**
424      * Resolve a category by ID number.
425      *
426      * @param string $idnumber category ID number.
427      * @return int category ID.
428      */
429     public static function resolve_category_by_idnumber($idnumber) {
430         global $DB;
431         $cache = cache::make('tool_uploadcourse', 'helper');
432         $cachekey = 'cat_idn_' . $idnumber;
433         if (($id = $cache->get($cachekey)) === false) {
434             $params = array('idnumber' => $idnumber);
435             $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
437             // Little hack to be able to differenciate between the cache not set and a category not found.
438             if ($id === false) {
439                 $id = -1;
440             }
442             $cache->set($cachekey, $id);
443         }
445         // Little hack to be able to differenciate between the cache not set and a category not found.
446         if ($id == -1) {
447             $id = false;
448         }
450         return $id;
451     }
453     /**
454      * Resolve a category by path.
455      *
456      * @param array $path category names indexed from parent to children.
457      * @return int category ID.
458      */
459     public static function resolve_category_by_path(array $path) {
460         global $DB;
461         $cache = cache::make('tool_uploadcourse', 'helper');
462         $cachekey = 'cat_path_' . serialize($path);
463         if (($id = $cache->get($cachekey)) === false) {
464             $parent = 0;
465             $sql = 'name = :name AND parent = :parent';
466             while ($name = array_shift($path)) {
467                 $params = array('name' => $name, 'parent' => $parent);
468                 if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
469                     if (count($records) > 1) {
470                         // Too many records with the same name!
471                         $id = -1;
472                         break;
473                     }
474                     $record = reset($records);
475                     $id = $record->id;
476                     $parent = $record->id;
477                 } else {
478                     // Not found.
479                     $id = -1;
480                     break;
481                 }
482             }
483             $cache->set($cachekey, $id);
484         }
486         // We save -1 when the category has not been found to be able to know if the cache was set.
487         if ($id == -1) {
488             $id = false;
489         }
490         return $id;
491     }