1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Bulk user registration functions
19 *
20 * @package    tool
21 * @subpackage uploaduser
22 * @copyright  2004 onwards Martin Dougiamas (http://dougiamas.com)
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26defined('MOODLE_INTERNAL') || die();
27
28define('UU_USER_ADDNEW', 0);
29define('UU_USER_ADDINC', 1);
30define('UU_USER_ADD_UPDATE', 2);
31define('UU_USER_UPDATE', 3);
32
33define('UU_UPDATE_NOCHANGES', 0);
34define('UU_UPDATE_FILEOVERRIDE', 1);
35define('UU_UPDATE_ALLOVERRIDE', 2);
36define('UU_UPDATE_MISSING', 3);
37
38define('UU_BULK_NONE', 0);
39define('UU_BULK_NEW', 1);
40define('UU_BULK_UPDATED', 2);
41define('UU_BULK_ALL', 3);
42
43define('UU_PWRESET_NONE', 0);
44define('UU_PWRESET_WEAK', 1);
45define('UU_PWRESET_ALL', 2);
46
47/**
48 * Tracking of processed users.
49 *
50 * This class prints user information into a html table.
51 *
52 * @package    core
53 * @subpackage admin
54 * @copyright  2007 Petr Skoda  {@link http://skodak.org}
55 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
56 */
57class uu_progress_tracker {
58    /** @var array */
59    protected $_row;
60
61    /**
62     * The columns shown on the table.
63     * @var array
64     */
65    public $columns = [];
66    /** @var array column headers */
67    protected $headers = [];
68
69    /**
70     * uu_progress_tracker constructor.
71     */
72    public function __construct() {
73        $this->headers = [
74            'status' => get_string('status'),
75            'line' => get_string('uucsvline', 'tool_uploaduser'),
76            'id' => 'ID',
77            'username' => get_string('username'),
78            'firstname' => get_string('firstname'),
79            'lastname' => get_string('lastname'),
80            'email' => get_string('email'),
81            'password' => get_string('password'),
82            'auth' => get_string('authentication'),
83            'enrolments' => get_string('enrolments', 'enrol'),
84            'suspended' => get_string('suspended', 'auth'),
85            'theme' => get_string('theme'),
86            'deleted' => get_string('delete'),
87        ];
88        $this->columns = array_keys($this->headers);
89    }
90
91    /**
92     * Print table header.
93     * @return void
94     */
95    public function start() {
96        $ci = 0;
97        echo '<table id="uuresults" class="generaltable boxaligncenter flexible-wrap" summary="'.get_string('uploadusersresult', 'tool_uploaduser').'">';
98        echo '<tr class="heading r0">';
99        foreach ($this->headers as $key => $header) {
100            echo '<th class="header c'.$ci++.'" scope="col">'.$header.'</th>';
101        }
102        echo '</tr>';
103        $this->_row = null;
104    }
105
106    /**
107     * Flush previous line and start a new one.
108     * @return void
109     */
110    public function flush() {
111        if (empty($this->_row) or empty($this->_row['line']['normal'])) {
112            // Nothing to print - each line has to have at least number
113            $this->_row = array();
114            foreach ($this->columns as $col) {
115                $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>'');
116            }
117            return;
118        }
119        $ci = 0;
120        $ri = 1;
121        echo '<tr class="r'.$ri.'">';
122        foreach ($this->_row as $key=>$field) {
123            foreach ($field as $type=>$content) {
124                if ($field[$type] !== '') {
125                    $field[$type] = '<span class="uu'.$type.'">'.$field[$type].'</span>';
126                } else {
127                    unset($field[$type]);
128                }
129            }
130            echo '<td class="cell c'.$ci++.'">';
131            if (!empty($field)) {
132                echo implode('<br />', $field);
133            } else {
134                echo '&nbsp;';
135            }
136            echo '</td>';
137        }
138        echo '</tr>';
139        foreach ($this->columns as $col) {
140            $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>'');
141        }
142    }
143
144    /**
145     * Add tracking info
146     * @param string $col name of column
147     * @param string $msg message
148     * @param string $level 'normal', 'warning' or 'error'
149     * @param bool $merge true means add as new line, false means override all previous text of the same type
150     * @return void
151     */
152    public function track($col, $msg, $level = 'normal', $merge = true) {
153        if (empty($this->_row)) {
154            $this->flush(); //init arrays
155        }
156        if (!in_array($col, $this->columns)) {
157            debugging('Incorrect column:'.$col);
158            return;
159        }
160        if ($merge) {
161            if ($this->_row[$col][$level] != '') {
162                $this->_row[$col][$level] .='<br />';
163            }
164            $this->_row[$col][$level] .= $msg;
165        } else {
166            $this->_row[$col][$level] = $msg;
167        }
168    }
169
170    /**
171     * Print the table end
172     * @return void
173     */
174    public function close() {
175        $this->flush();
176        echo '</table>';
177    }
178}
179
180/**
181 * Validation callback function - verified the column line of csv file.
182 * Converts standard column names to lowercase.
183 * @param csv_import_reader $cir
184 * @param array $stdfields standard user fields
185 * @param array $profilefields custom profile fields
186 * @param moodle_url $returnurl return url in case of any error
187 * @return array list of fields
188 */
189function uu_validate_user_upload_columns(csv_import_reader $cir, $stdfields, $profilefields, moodle_url $returnurl) {
190    $columns = $cir->get_columns();
191
192    if (empty($columns)) {
193        $cir->close();
194        $cir->cleanup();
195        print_error('cannotreadtmpfile', 'error', $returnurl);
196    }
197    if (count($columns) < 2) {
198        $cir->close();
199        $cir->cleanup();
200        print_error('csvfewcolumns', 'error', $returnurl);
201    }
202
203    // test columns
204    $processed = array();
205    foreach ($columns as $key=>$unused) {
206        $field = $columns[$key];
207        $field = trim($field);
208        $lcfield = core_text::strtolower($field);
209        if (in_array($field, $stdfields) or in_array($lcfield, $stdfields)) {
210            // standard fields are only lowercase
211            $newfield = $lcfield;
212
213        } else if (in_array($field, $profilefields)) {
214            // exact profile field name match - these are case sensitive
215            $newfield = $field;
216
217        } else if (in_array($lcfield, $profilefields)) {
218            // hack: somebody wrote uppercase in csv file, but the system knows only lowercase profile field
219            $newfield = $lcfield;
220
221        } else if (preg_match('/^(sysrole|cohort|course|group|type|role|enrolperiod|enrolstatus|enroltimestart)\d+$/', $lcfield)) {
222            // special fields for enrolments
223            $newfield = $lcfield;
224
225        } else {
226            $cir->close();
227            $cir->cleanup();
228            print_error('invalidfieldname', 'error', $returnurl, $field);
229        }
230        if (in_array($newfield, $processed)) {
231            $cir->close();
232            $cir->cleanup();
233            print_error('duplicatefieldname', 'error', $returnurl, $newfield);
234        }
235        $processed[$key] = $newfield;
236    }
237
238    return $processed;
239}
240
241/**
242 * Increments username - increments trailing number or adds it if not present.
243 * Varifies that the new username does not exist yet
244 * @param string $username
245 * @return incremented username which does not exist yet
246 */
247function uu_increment_username($username) {
248    global $DB, $CFG;
249
250    if (!preg_match_all('/(.*?)([0-9]+)$/', $username, $matches)) {
251        $username = $username.'2';
252    } else {
253        $username = $matches[1][0].($matches[2][0]+1);
254    }
255
256    if ($DB->record_exists('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
257        return uu_increment_username($username);
258    } else {
259        return $username;
260    }
261}
262
263/**
264 * Check if default field contains templates and apply them.
265 * @param string template - potential tempalte string
266 * @param object user object- we need username, firstname and lastname
267 * @return string field value
268 */
269function uu_process_template($template, $user) {
270    if (is_array($template)) {
271        // hack for for support of text editors with format
272        $t = $template['text'];
273    } else {
274        $t = $template;
275    }
276    if (strpos($t, '%') === false) {
277        return $template;
278    }
279
280    $username  = isset($user->username)  ? $user->username  : '';
281    $firstname = isset($user->firstname) ? $user->firstname : '';
282    $lastname  = isset($user->lastname)  ? $user->lastname  : '';
283
284    $callback = partial('uu_process_template_callback', $username, $firstname, $lastname);
285
286    $result = preg_replace_callback('/(?<!%)%([+-~])?(\d)*([flu])/', $callback, $t);
287
288    if (is_null($result)) {
289        return $template; //error during regex processing??
290    }
291
292    if (is_array($template)) {
293        $template['text'] = $result;
294        return $t;
295    } else {
296        return $result;
297    }
298}
299
300/**
301 * Internal callback function.
302 */
303function uu_process_template_callback($username, $firstname, $lastname, $block) {
304    switch ($block[3]) {
305        case 'u':
306            $repl = $username;
307            break;
308        case 'f':
309            $repl = $firstname;
310            break;
311        case 'l':
312            $repl = $lastname;
313            break;
314        default:
315            return $block[0];
316    }
317
318    switch ($block[1]) {
319        case '+':
320            $repl = core_text::strtoupper($repl);
321            break;
322        case '-':
323            $repl = core_text::strtolower($repl);
324            break;
325        case '~':
326            $repl = core_text::strtotitle($repl);
327            break;
328    }
329
330    if (!empty($block[2])) {
331        $repl = core_text::substr($repl, 0 , $block[2]);
332    }
333
334    return $repl;
335}
336
337/**
338 * Returns list of auth plugins that are enabled and known to work.
339 *
340 * If ppl want to use some other auth type they have to include it
341 * in the CSV file next on each line.
342 *
343 * @return array type=>name
344 */
345function uu_supported_auths() {
346    // Get all the enabled plugins.
347    $plugins = get_enabled_auth_plugins();
348    $choices = array();
349    foreach ($plugins as $plugin) {
350        $objplugin = get_auth_plugin($plugin);
351        // If the plugin can not be manually set skip it.
352        if (!$objplugin->can_be_manually_set()) {
353            continue;
354        }
355        $choices[$plugin] = get_string('pluginname', "auth_{$plugin}");
356    }
357
358    return $choices;
359}
360
361/**
362 * Returns list of roles that are assignable in courses
363 * @return array
364 */
365function uu_allowed_roles() {
366    // let's cheat a bit, frontpage is guaranteed to exist and has the same list of roles ;-)
367    $roles = get_assignable_roles(context_course::instance(SITEID), ROLENAME_ORIGINALANDSHORT);
368    return array_reverse($roles, true);
369}
370
371/**
372 * Returns mapping of all roles using short role name as index.
373 * @return array
374 */
375function uu_allowed_roles_cache() {
376    $allowedroles = get_assignable_roles(context_course::instance(SITEID), ROLENAME_SHORT);
377    $rolecache = [];
378    foreach ($allowedroles as $rid=>$rname) {
379        $rolecache[$rid] = new stdClass();
380        $rolecache[$rid]->id   = $rid;
381        $rolecache[$rid]->name = $rname;
382        if (!is_numeric($rname)) { // only non-numeric shortnames are supported!!!
383            $rolecache[$rname] = new stdClass();
384            $rolecache[$rname]->id   = $rid;
385            $rolecache[$rname]->name = $rname;
386        }
387    }
388    return $rolecache;
389}
390
391/**
392 * Returns mapping of all system roles using short role name as index.
393 * @return array
394 */
395function uu_allowed_sysroles_cache() {
396    $allowedroles = get_assignable_roles(context_system::instance(), ROLENAME_SHORT);
397    $rolecache = [];
398    foreach ($allowedroles as $rid => $rname) {
399        $rolecache[$rid] = new stdClass();
400        $rolecache[$rid]->id   = $rid;
401        $rolecache[$rid]->name = $rname;
402        if (!is_numeric($rname)) { // Only non-numeric shortnames are supported!
403            $rolecache[$rname] = new stdClass();
404            $rolecache[$rname]->id   = $rid;
405            $rolecache[$rname]->name = $rname;
406        }
407    }
408    return $rolecache;
409}
410
411/**
412 * Pre process custom profile data, and update it with corrected value
413 *
414 * @param stdClass $data user profile data
415 * @return stdClass pre-processed custom profile data
416 */
417function uu_pre_process_custom_profile_data($data) {
418    global $CFG;
419    require_once($CFG->dirroot . '/user/profile/lib.php');
420    $fields = profile_get_user_fields_with_data(0);
421
422    // find custom profile fields and check if data needs to converted.
423    foreach ($data as $key => $value) {
424        if (preg_match('/^profile_field_/', $key)) {
425            $shortname = str_replace('profile_field_', '', $key);
426            if ($fields) {
427                foreach ($fields as $formfield) {
428                    if ($formfield->get_shortname() === $shortname && method_exists($formfield, 'convert_external_data')) {
429                        $data->$key = $formfield->convert_external_data($value);
430                    }
431                }
432            }
433        }
434    }
435    return $data;
436}
437
438/**
439 * Checks if data provided for custom fields is correct
440 * Currently checking for custom profile field or type menu
441 *
442 * @param array $data user profile data
443 * @return bool true if no error else false
444 */
445function uu_check_custom_profile_data(&$data) {
446    global $CFG;
447    require_once($CFG->dirroot.'/user/profile/lib.php');
448
449    $noerror = true;
450    $testuserid = null;
451
452    if (!empty($data['username'])) {
453        if (preg_match('/id=(.*)"/i', $data['username'], $result)) {
454            $testuserid = $result[1];
455        }
456    }
457    $profilefields = profile_get_user_fields_with_data(0);
458    // Find custom profile fields and check if data needs to converted.
459    foreach ($data as $key => $value) {
460        if (preg_match('/^profile_field_/', $key)) {
461            $shortname = str_replace('profile_field_', '', $key);
462            foreach ($profilefields as $formfield) {
463                if ($formfield->get_shortname() === $shortname) {
464                    if (method_exists($formfield, 'convert_external_data') &&
465                            is_null($formfield->convert_external_data($value))) {
466                        $data['status'][] = get_string('invaliduserfield', 'error', $shortname);
467                        $noerror = false;
468                    }
469                    // Check for duplicate value.
470                    if (method_exists($formfield, 'edit_validate_field') ) {
471                        $testuser = new stdClass();
472                        $testuser->{$key} = $value;
473                        $testuser->id = $testuserid;
474                        $err = $formfield->edit_validate_field($testuser);
475                        if (!empty($err[$key])) {
476                            $data['status'][] = $err[$key].' ('.$key.')';
477                            $noerror = false;
478                        }
479                    }
480                }
481            }
482        }
483    }
484    return $noerror;
485}
486