xref: /dokuwiki/lib/plugins/authpdo/auth.php (revision 4fb8dfab9fa5bc3e6249a16d39713415988a5ec2)
1<?php
2/**
3 * DokuWiki Plugin authpdo (Auth Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr <andi@splitbrain.org>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12class auth_plugin_authpdo extends DokuWiki_Auth_Plugin {
13
14    /** @var PDO */
15    protected $pdo;
16
17    /**
18     * Constructor.
19     */
20    public function __construct() {
21        parent::__construct(); // for compatibility
22
23        if(!class_exists('PDO')) {
24            $this->_debug('PDO extension for PHP not found.', -1, __LINE__);
25            $this->success = false;
26            return;
27        }
28
29        if(!$this->getConf('dsn')) {
30            $this->_debug('No DSN specified', -1, __LINE__);
31            $this->success = false;
32            return;
33        }
34
35        try {
36            $this->pdo = new PDO(
37                $this->getConf('dsn'),
38                $this->getConf('user'),
39                $this->getConf('pass'),
40                array(
41                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array
42                    PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names
43                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes
44                )
45            );
46        } catch(PDOException $e) {
47            $this->_debug($e);
48            $this->success = false;
49            return;
50        }
51
52        // FIXME set capabilities accordingly
53        //$this->cando['addUser']     = false; // can Users be created?
54        //$this->cando['delUser']     = false; // can Users be deleted?
55        //$this->cando['modLogin']    = false; // can login names be changed?
56        //$this->cando['modPass']     = false; // can passwords be changed?
57        //$this->cando['modName']     = false; // can real names be changed?
58        //$this->cando['modMail']     = false; // can emails be changed?
59        //$this->cando['modGroups']   = false; // can groups be changed?
60        //$this->cando['getUsers']    = false; // can a (filtered) list of users be retrieved?
61        //$this->cando['getUserCount']= false; // can the number of users be retrieved?
62        //$this->cando['getGroups']   = false; // can a list of available groups be retrieved?
63        //$this->cando['external']    = false; // does the module do external auth checking?
64        //$this->cando['logout']      = true; // can the user logout again? (eg. not possible with HTTP auth)
65
66        // FIXME intialize your auth system and set success to true, if successful
67        $this->success = true;
68    }
69
70    /**
71     * Check user+password
72     *
73     * @param   string $user the user name
74     * @param   string $pass the clear text password
75     * @return  bool
76     */
77    public function checkPass($user, $pass) {
78
79        $data = $this->_selectUser($user);
80        if($data == false) return false;
81
82        if(isset($data['hash'])) {
83            // hashed password
84            $passhash = new PassHash();
85            return $passhash->verify_hash($pass, $data['hash']);
86        } else {
87            // clear text password in the database O_o
88            return ($pass == $data['clear']);
89        }
90    }
91
92    /**
93     * Return user info
94     *
95     * Returns info about the given user needs to contain
96     * at least these fields:
97     *
98     * name string  full name of the user
99     * mail string  email addres of the user
100     * grps array   list of groups the user is in
101     *
102     * @param   string $user the user name
103     * @param   bool $requireGroups whether or not the returned data must include groups
104     * @return array containing user data or false
105     */
106    public function getUserData($user, $requireGroups = true) {
107        $data = $this->_selectUser($user);
108        if($data == false) return false;
109
110        if(isset($data['hash'])) unset($data['hash']);
111        if(isset($data['clean'])) unset($data['clean']);
112
113        if($requireGroups) {
114            $data['grps'] = $this->_selectUserGroups($data);
115            if($data['grps'] === false) return false;
116        }
117
118        return $data;
119    }
120
121    /**
122     * Create a new User [implement only where required/possible]
123     *
124     * Returns false if the user already exists, null when an error
125     * occurred and true if everything went well.
126     *
127     * The new user HAS TO be added to the default group by this
128     * function!
129     *
130     * Set addUser capability when implemented
131     *
132     * @param  string $user
133     * @param  string $clear
134     * @param  string $name
135     * @param  string $mail
136     * @param  null|array $grps
137     * @return bool|null
138     */
139    public function createUser($user, $clear, $name, $mail, $grps = null) {
140        global $conf;
141
142        if(($info = $this->getUserData($user, false)) !== false) {
143            msg($this->getLang('userexists'), -1);
144            return false; // user already exists
145        }
146
147        // prepare data
148        if($grps == null) $grps = array();
149        $grps[] = $conf['defaultgroup'];
150        $grps = array_unique($grps);
151        $hash = auth_cryptPassword($clear);
152        $userdata = compact('user', 'clear', 'hash', 'name', 'mail');
153
154        // action protected by transaction
155        $this->pdo->beginTransaction();
156        {
157            // insert the user
158            $ok = $this->_query($this->getConf('insert-user'), $userdata);
159            if($ok === false) goto FAIL;
160            $userdata = $this->getUserData($user, false);
161            if($userdata === false) goto FAIL;
162
163            // create all groups that do not exist, the refetch the groups
164            $allgroups = $this->_selectGroups();
165            foreach($grps as $group) {
166                if(!isset($allgroups[$group])) {
167                    $ok = $this->_insertGroup($group);
168                    if($ok === false) goto FAIL;
169                }
170            }
171            $allgroups = $this->_selectGroups();
172
173            // add user to the groups
174            foreach($grps as $group) {
175                $ok = $this->_joinGroup($userdata, $allgroups[$group]);
176                if($ok === false) goto FAIL;
177            }
178        }
179        $this->pdo->commit();
180        return true;
181
182        // something went wrong, rollback
183        FAIL:
184        $this->pdo->rollBack();
185        $this->_debug('Transaction rolled back', 0, __LINE__);
186        return null; // return error
187    }
188
189    /**
190     * Modify user data
191     *
192     * @param   string $user nick of the user to be changed
193     * @param   array $changes array of field/value pairs to be changed (password will be clear text)
194     * @return  bool
195     */
196    public function modifyUser($user, $changes) {
197        // secure everything in transaction
198        $this->pdo->beginTransaction();
199        {
200            $olddata = $this->getUserData($user);
201            $oldgroups = $olddata['grps'];
202            unset($olddata['grps']);
203
204            // changing the user name?
205            if(isset($changes['user'])) {
206                if($this->getUserData($changes['user'], false)) goto FAIL;
207                $params = $olddata;
208                $params['newlogin'] = $changes['user'];
209
210                $ok = $this->_query($this->getConf('update-user-login'), $params);
211                if($ok === false) goto FAIL;
212            }
213
214            // changing the password?
215            if(isset($changes['pass'])) {
216                $params = $olddata;
217                $params['clear'] = $changes['pass'];
218                $params['hash'] = auth_cryptPassword($changes['pass']);
219
220                $ok = $this->_query($this->getConf('update-user-pass'), $params);
221                if($ok === false) goto FAIL;
222            }
223
224            // changing info?
225            if(isset($changes['mail']) || isset($changes['name'])) {
226                $params = $olddata;
227                if(isset($changes['mail'])) $params['mail'] = $changes['mail'];
228                if(isset($changes['name'])) $params['name'] = $changes['name'];
229
230                $ok = $this->_query($this->getConf('update-user-info'), $params);
231                if($ok === false) goto FAIL;
232            }
233
234            // changing groups?
235            if(isset($changes['grps'])) {
236                $allgroups = $this->_selectGroups();
237
238                // remove membership for previous groups
239                foreach($oldgroups as $group) {
240                    if(!in_array($group, $changes['grps'])) {
241                        $ok = $this->_leaveGroup($olddata, $allgroups[$group]);
242                        if($ok === false) goto FAIL;
243                    }
244                }
245
246                // create all new groups that are missing
247                $added = 0;
248                foreach($changes['grps'] as $group) {
249                    if(!isset($allgroups[$group])) {
250                        $ok = $this->_insertGroup($group);
251                        if($ok === false) goto FAIL;
252                        $added++;
253                    }
254                }
255                // reload group info
256                if($added > 0) $allgroups = $this->_selectGroups();
257
258                // add membership for new groups
259                foreach($changes['grps'] as $group) {
260                    if(!in_array($group, $oldgroups)) {
261                        $ok = $this->_joinGroup($olddata, $allgroups[$group]);
262                        if($ok === false) goto FAIL;
263                    }
264                }
265            }
266
267        }
268        $this->pdo->commit();
269        return true;
270
271        // something went wrong, rollback
272        FAIL:
273        $this->pdo->rollBack();
274        $this->_debug('Transaction rolled back', 0, __LINE__);
275        return false; // return error
276    }
277
278    /**
279     * Delete one or more users [implement only where required/possible]
280     *
281     * Set delUser capability when implemented
282     *
283     * @param   array $users
284     * @return  int    number of users deleted
285     */
286    //public function deleteUsers($users) {
287    // FIXME implement
288    //    return false;
289    //}
290
291    /**
292     * Bulk retrieval of user data [implement only where required/possible]
293     *
294     * Set getUsers capability when implemented
295     *
296     * @param   int $start index of first user to be returned
297     * @param   int $limit max number of users to be returned
298     * @param   array $filter array of field/pattern pairs, null for no filter
299     * @return  array list of userinfo (refer getUserData for internal userinfo details)
300     */
301    //public function retrieveUsers($start = 0, $limit = -1, $filter = null) {
302    // FIXME implement
303    //    return array();
304    //}
305
306    /**
307     * Return a count of the number of user which meet $filter criteria
308     * [should be implemented whenever retrieveUsers is implemented]
309     *
310     * Set getUserCount capability when implemented
311     *
312     * @param  array $filter array of field/pattern pairs, empty array for no filter
313     * @return int
314     */
315    //public function getUserCount($filter = array()) {
316    // FIXME implement
317    //    return 0;
318    //}
319
320    /**
321     * Define a group [implement only where required/possible]
322     *
323     * Set addGroup capability when implemented
324     *
325     * @param   string $group
326     * @return  bool
327     */
328    //public function addGroup($group) {
329    // FIXME implement
330    //    return false;
331    //}
332
333    /**
334     * Retrieve groups
335     *
336     * Set getGroups capability when implemented
337     *
338     * @param   int $start
339     * @param   int $limit
340     * @return  array
341     */
342    public function retrieveGroups($start = 0, $limit = 0) {
343        $groups = array_keys($this->_selectGroups());
344        if($groups === false) return array();
345
346        if(!$limit) {
347            return array_splice($groups, $start);
348        } else {
349            return array_splice($groups, $start, $limit);
350        }
351    }
352
353    /**
354     * Select data of a specified user
355     *
356     * @param string $user the user name
357     * @return bool|array user data, false on error
358     */
359    protected function _selectUser($user) {
360        $sql = $this->getConf('select-user');
361
362        $result = $this->_query($sql, array(':user' => $user));
363        if(!$result) return false;
364
365        if(count($result) > 1) {
366            $this->_debug('Found more than one matching user', -1, __LINE__);
367            return false;
368        }
369
370        $data = array_shift($result);
371        $dataok = true;
372
373        if(!isset($data['user'])) {
374            $this->_debug("Statement did not return 'user' attribute", -1, __LINE__);
375            $dataok = false;
376        }
377        if(!isset($data['hash']) && !isset($data['clear'])) {
378            $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__);
379            $dataok = false;
380        }
381        if(!isset($data['name'])) {
382            $this->_debug("Statement did not return 'name' attribute", -1, __LINE__);
383            $dataok = false;
384        }
385        if(!isset($data['mail'])) {
386            $this->_debug("Statement did not return 'mail' attribute", -1, __LINE__);
387            $dataok = false;
388        }
389
390        if(!$dataok) return false;
391        return $data;
392    }
393
394    /**
395     * Select all groups of a user
396     *
397     * @param array $userdata The userdata as returned by _selectUser()
398     * @return array|bool list of group names, false on error
399     */
400    protected function _selectUserGroups($userdata) {
401        global $conf;
402        $sql = $this->getConf('select-user-groups');
403        $result = $this->_query($sql, $userdata);
404        if($result === false) return false;
405
406        $groups = array($conf['defaultgroup']); // always add default config
407        foreach($result as $row) {
408            if(!isset($row['group'])) {
409                $this->_debug("No 'group' field returned in select-user-groups statement");
410                return false;
411            }
412            $groups[] = $row['group'];
413        }
414
415        $groups = array_unique($groups);
416        sort($groups);
417        return $groups;
418    }
419
420    /**
421     * Select all available groups
422     *
423     * @todo this should be cached
424     * @return array|bool list of all available groups and their properties
425     */
426    protected function _selectGroups() {
427        $sql = $this->getConf('select-groups');
428        $result = $this->_query($sql);
429        if($result === false) return false;
430
431        $groups = array();
432        foreach($result as $row) {
433            if(!isset($row['group'])) {
434                $this->_debug("No 'group' field returned from select-groups statement", -1, __LINE__);
435                return false;
436            }
437
438            // relayout result with group name as key
439            $group = $row['group'];
440            $groups[$group] = $row;
441        }
442
443        ksort($groups);
444        return $groups;
445    }
446
447    /**
448     * Create a new group with the given name
449     *
450     * @param string $group
451     * @return bool
452     */
453    protected function _insertGroup($group) {
454        $sql = $this->getConf('insert-group');
455
456        $result = $this->_query($sql, array(':group' => $group));
457        if($result === false) return false;
458        return true;
459    }
460
461    /**
462     * Adds the user to the group
463     *
464     * @param array $userdata all the user data
465     * @param array $groupdata all the group data
466     * @return bool
467     */
468    protected function _joinGroup($userdata, $groupdata) {
469        $data = array_merge($userdata, $groupdata);
470        $sql = $this->getConf('join-group');
471        $result = $this->_query($sql, $data);
472        if($result === false) return false;
473        return true;
474    }
475
476    /**
477     * Removes the user from the group
478     *
479     * @param array $userdata all the user data
480     * @param array $groupdata all the group data
481     * @return bool
482     */
483    protected function _leaveGroup($userdata, $groupdata) {
484        $data = array_merge($userdata, $groupdata);
485        $sql = $this->getConf('leave-group');
486        $result = $this->_query($sql, $data);
487        if($result === false) return false;
488        return true;
489    }
490
491    /**
492     * Executes a query
493     *
494     * @param string $sql The SQL statement to execute
495     * @param array $arguments Named parameters to be used in the statement
496     * @return array|bool The result as associative array, false on error
497     */
498    protected function _query($sql, $arguments = array()) {
499        if(empty($sql)) {
500            $this->_debug('No SQL query given', -1, __LINE__);
501            return false;
502        }
503
504        // prepare parameters - we only use those that exist in the SQL
505        $params = array();
506        foreach($arguments as $key => $value) {
507            if(is_array($value)) continue;
508            if(is_object($value)) continue;
509            if($key[0] != ':') $key = ":$key"; // prefix with colon if needed
510            if(strpos($sql, $key) !== false) $params[$key] = $value;
511        }
512
513        // execute
514        $sth = $this->pdo->prepare($sql);
515        try {
516            $sth->execute($params);
517            $result = $sth->fetchAll();
518        } catch(Exception $e) {
519            // report the caller's line
520            $trace = debug_backtrace();
521            $line = $trace[0]['line'];
522            $dsql = $this->_debugSQL($sql, $params, !defined('DOKU_UNITTEST'));
523            $this->_debug($e, -1, $line);
524            $this->_debug("SQL: <pre>$dsql</pre>", -1, $line);
525            $result = false;
526        } finally {
527            $sth->closeCursor();
528            $sth = null;
529        }
530
531        return $result;
532    }
533
534    /**
535     * Wrapper around msg() but outputs only when debug is enabled
536     *
537     * @param string|Exception $message
538     * @param int $err
539     * @param int $line
540     */
541    protected function _debug($message, $err = 0, $line = 0) {
542        if(!$this->getConf('debug')) return;
543        if(is_a($message, 'Exception')) {
544            $err = -1;
545            $msg = $message->getMessage();
546            if(!$line) $line = $message->getLine();
547        } else {
548            $msg = $message;
549        }
550
551        if(defined('DOKU_UNITTEST')) {
552            printf("\n%s, %s:%d\n", $msg, __FILE__, $line);
553        } else {
554            msg('authpdo: ' . $msg, $err, $line, __FILE__);
555        }
556    }
557
558    /**
559     * Check if the given config strings are set
560     *
561     * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
562     *
563     * @param   string[] $keys
564     * @return  bool
565     */
566    protected function _chkcnf($keys) {
567        foreach($keys as $key) {
568            if(!$this->getConf($key)) return false;
569        }
570
571        return true;
572    }
573
574    /**
575     * create an approximation of the SQL string with parameters replaced
576     *
577     * @param string $sql
578     * @param array $params
579     * @param bool $htmlescape Should the result be escaped for output in HTML?
580     * @return string
581     */
582    protected function _debugSQL($sql, $params, $htmlescape = true) {
583        foreach($params as $key => $val) {
584            if(is_int($val)) {
585                $val = $this->pdo->quote($val, PDO::PARAM_INT);
586            } elseif(is_bool($val)) {
587                $val = $this->pdo->quote($val, PDO::PARAM_BOOL);
588            } elseif(is_null($val)) {
589                $val = 'NULL';
590            } else {
591                $val = $this->pdo->quote($val);
592            }
593            $sql = str_replace($key, $val, $sql);
594        }
595        if($htmlescape) $sql = hsc($sql);
596        return $sql;
597    }
598}
599
600// vim:ts=4:sw=4:et:
601