xref: /dokuwiki/lib/plugins/authpdo/auth.php (revision 5de3a6a5ad40cfd5f62c9a028f248d53e3f9da6d)
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     * May be ommited if trustExternal is used.
74     *
75     * @param   string $user the user name
76     * @param   string $pass the clear text password
77     * @return  bool
78     */
79    public function checkPass($user, $pass) {
80
81        $data = $this->_selectUser($user);
82        if($data == false) return false;
83
84        if(isset($data['hash'])) {
85            // hashed password
86            $passhash = new PassHash();
87            return $passhash->verify_hash($pass, $data['hash']);
88        } else {
89            // clear text password in the database O_o
90            return ($pass == $data['clear']);
91        }
92    }
93
94    /**
95     * Return user info
96     *
97     * Returns info about the given user needs to contain
98     * at least these fields:
99     *
100     * name string  full name of the user
101     * mail string  email addres of the user
102     * grps array   list of groups the user is in
103     *
104     * @param   string $user the user name
105     * @param   bool $requireGroups whether or not the returned data must include groups
106     * @return array containing user data or false
107     */
108    public function getUserData($user, $requireGroups = true) {
109        $data = $this->_selectUser($user);
110        if($data == false) return false;
111
112        if(isset($data['hash'])) unset($data['hash']);
113        if(isset($data['clean'])) unset($data['clean']);
114
115        if($requireGroups) {
116            $data['grps'] = $this->_selectUserGroups($data);
117            if($data['grps'] === false) return false;
118        }
119
120        return $data;
121    }
122
123    /**
124     * Create a new User [implement only where required/possible]
125     *
126     * Returns false if the user already exists, null when an error
127     * occurred and true if everything went well.
128     *
129     * The new user HAS TO be added to the default group by this
130     * function!
131     *
132     * Set addUser capability when implemented
133     *
134     * @param  string $user
135     * @param  string $clear
136     * @param  string $name
137     * @param  string $mail
138     * @param  null|array $grps
139     * @return bool|null
140     */
141    public function createUser($user, $clear, $name, $mail, $grps = null) {
142        global $conf;
143
144        if(($info = $this->getUserData($user, false)) !== false) {
145            msg($this->getLang('userexists'), -1);
146            return false; // user already exists
147        }
148
149        // prepare data
150        if($grps == null) $grps = array();
151        $grps[] = $conf['defaultgroup'];
152        $grps = array_unique($grps);
153        $hash = auth_cryptPassword($clear);
154        $userdata = compact('user', 'clear', 'hash', 'name', 'mail');
155
156        // action protected by transaction
157        $this->pdo->beginTransaction();
158        {
159            // insert the user
160            $ok = $this->_query($this->getConf('insert-user'), $userdata);
161            if($ok === false) goto FAIL;
162            $userdata = $this->getUserData($user, false);
163            if($userdata === false) goto FAIL;
164
165            // create all groups that do not exist, the refetch the groups
166            $allgroups = $this->_selectGroups();
167            foreach($grps as $group) {
168                if(!isset($allgroups[$group])) {
169                    $ok = $this->_insertGroup($group);
170                    if($ok === false) goto FAIL;
171                }
172            }
173            $allgroups = $this->_selectGroups();
174
175            // add user to the groups
176            foreach($grps as $group) {
177                $ok = $this->_joinGroup($userdata, $allgroups[$group]);
178                if($ok === false) goto FAIL;
179            }
180        }
181        $this->pdo->commit();
182        return true;
183
184        // something went wrong, rollback
185        FAIL:
186        $this->pdo->rollBack();
187        $this->_debug('Transaction rolled back', 0, __LINE__);
188        return null; // return error
189    }
190
191    /**
192     * Modify user data [implement only where required/possible]
193     *
194     * Set the mod* capabilities according to the implemented features
195     *
196     * @param   string $user nick of the user to be changed
197     * @param   array $changes array of field/value pairs to be changed (password will be clear text)
198     * @return  bool
199     */
200    //public function modifyUser($user, $changes) {
201    // FIXME implement
202    //    return false;
203    //}
204
205    /**
206     * Delete one or more users [implement only where required/possible]
207     *
208     * Set delUser capability when implemented
209     *
210     * @param   array $users
211     * @return  int    number of users deleted
212     */
213    //public function deleteUsers($users) {
214    // FIXME implement
215    //    return false;
216    //}
217
218    /**
219     * Bulk retrieval of user data [implement only where required/possible]
220     *
221     * Set getUsers capability when implemented
222     *
223     * @param   int $start index of first user to be returned
224     * @param   int $limit max number of users to be returned
225     * @param   array $filter array of field/pattern pairs, null for no filter
226     * @return  array list of userinfo (refer getUserData for internal userinfo details)
227     */
228    //public function retrieveUsers($start = 0, $limit = -1, $filter = null) {
229    // FIXME implement
230    //    return array();
231    //}
232
233    /**
234     * Return a count of the number of user which meet $filter criteria
235     * [should be implemented whenever retrieveUsers is implemented]
236     *
237     * Set getUserCount capability when implemented
238     *
239     * @param  array $filter array of field/pattern pairs, empty array for no filter
240     * @return int
241     */
242    //public function getUserCount($filter = array()) {
243    // FIXME implement
244    //    return 0;
245    //}
246
247    /**
248     * Define a group [implement only where required/possible]
249     *
250     * Set addGroup capability when implemented
251     *
252     * @param   string $group
253     * @return  bool
254     */
255    //public function addGroup($group) {
256    // FIXME implement
257    //    return false;
258    //}
259
260    /**
261     * Retrieve groups
262     *
263     * Set getGroups capability when implemented
264     *
265     * @param   int $start
266     * @param   int $limit
267     * @return  array
268     */
269    public function retrieveGroups($start = 0, $limit = 0) {
270        $groups = array_keys($this->_selectGroups());
271        if($groups === false) return array();
272
273        if(!$limit) {
274            return array_splice($groups, $start);
275        } else {
276            return array_splice($groups, $start, $limit);
277        }
278    }
279
280    /**
281     * Select data of a specified user
282     *
283     * @param string $user the user name
284     * @return bool|array user data, false on error
285     */
286    protected function _selectUser($user) {
287        $sql = $this->getConf('select-user');
288
289        $result = $this->_query($sql, array(':user' => $user));
290        if(!$result) return false;
291
292        if(count($result) > 1) {
293            $this->_debug('Found more than one matching user', -1, __LINE__);
294            return false;
295        }
296
297        $data = array_shift($result);
298        $dataok = true;
299
300        if(!isset($data['user'])) {
301            $this->_debug("Statement did not return 'user' attribute", -1, __LINE__);
302            $dataok = false;
303        }
304        if(!isset($data['hash']) && !isset($data['clear'])) {
305            $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__);
306            $dataok = false;
307        }
308        if(!isset($data['name'])) {
309            $this->_debug("Statement did not return 'name' attribute", -1, __LINE__);
310            $dataok = false;
311        }
312        if(!isset($data['mail'])) {
313            $this->_debug("Statement did not return 'mail' attribute", -1, __LINE__);
314            $dataok = false;
315        }
316
317        if(!$dataok) return false;
318        return $data;
319    }
320
321    /**
322     * Select all groups of a user
323     *
324     * @param array $userdata The userdata as returned by _selectUser()
325     * @return array|bool list of group names, false on error
326     */
327    protected function _selectUserGroups($userdata) {
328        global $conf;
329        $sql = $this->getConf('select-user-groups');
330        $result = $this->_query($sql, $userdata);
331        if($result === false) return false;
332
333        $groups = array($conf['defaultgroup']); // always add default config
334        foreach($result as $row) {
335            if(!isset($row['group'])) {
336                $this->_debug("No 'group' field returned in select-user-groups statement");
337                return false;
338            }
339            $groups[] = $row['group'];
340        }
341
342        $groups = array_unique($groups);
343        sort($groups);
344        return $groups;
345    }
346
347    /**
348     * Select all available groups
349     *
350     * @todo this should be cached
351     * @return array|bool list of all available groups and their properties
352     */
353    protected function _selectGroups() {
354        $sql = $this->getConf('select-groups');
355        $result = $this->_query($sql);
356        if($result === false) return false;
357
358        $groups = array();
359        foreach($result as $row) {
360            if(!isset($row['group'])) {
361                $this->_debug("No 'group' field returned from select-groups statement", -1, __LINE__);
362                return false;
363            }
364
365            // relayout result with group name as key
366            $group = $row['group'];
367            $groups[$group] = $row;
368        }
369
370        ksort($groups);
371        return $groups;
372    }
373
374    /**
375     * Create a new group with the given name
376     *
377     * @param string $group
378     * @return bool
379     */
380    protected function _insertGroup($group) {
381        $sql = $this->getConf('insert-group');
382
383        $result = $this->_query($sql, array(':group' => $group));
384        if($result === false) return false;
385        return true;
386    }
387
388    /**
389     * Enters the user to the group
390     *
391     * @param array $userdata all the user data
392     * @param array $groupdata all the group data
393     * @return bool
394     */
395    protected function _joinGroup($userdata, $groupdata) {
396        $data = array_merge($userdata, $groupdata);
397        $sql = $this->getConf('join-group');
398        $result = $this->_query($sql, $data);
399        if($result === false) return false;
400        return true;
401    }
402
403    /**
404     * Executes a query
405     *
406     * @param string $sql The SQL statement to execute
407     * @param array $arguments Named parameters to be used in the statement
408     * @return array|bool The result as associative array, false on error
409     */
410    protected function _query($sql, $arguments = array()) {
411        if(empty($sql)) {
412            $this->_debug('No SQL query given', -1, __LINE__);
413            return false;
414        }
415
416        // prepare parameters - we only use those that exist in the SQL
417        $params = array();
418        foreach($arguments as $key => $value) {
419            if(is_array($value)) continue;
420            if(is_object($value)) continue;
421            if($key[0] != ':') $key = ":$key"; // prefix with colon if needed
422            if(strpos($sql, $key) !== false) $params[$key] = $value;
423        }
424
425        // execute
426        $sth = $this->pdo->prepare($sql);
427        try {
428            $sth->execute($params);
429            $result = $sth->fetchAll();
430        } catch(Exception $e) {
431            $dsql = $this->_debugSQL($sql, $params, !defined('DOKU_UNITTEST'));
432            $this->_debug($e);
433            $this->_debug("SQL: <pre>$dsql</pre>", -1, $e->getLine());
434            $result = false;
435        } finally {
436            $sth->closeCursor();
437            $sth = null;
438        }
439
440        return $result;
441    }
442
443    /**
444     * Wrapper around msg() but outputs only when debug is enabled
445     *
446     * @param string|Exception $message
447     * @param int $err
448     * @param int $line
449     */
450    protected function _debug($message, $err = 0, $line = 0) {
451        if(!$this->getConf('debug')) return;
452        if(is_a($message, 'Exception')) {
453            $err = -1;
454            $line = $message->getLine();
455            $msg = $message->getMessage();
456        } else {
457            $msg = $message;
458        }
459
460        if(defined('DOKU_UNITTEST')) {
461            printf("\n%s, %s:%d\n", $msg, __FILE__, $line);
462        } else {
463            msg('authpdo: ' . $msg, $err, $line, __FILE__);
464        }
465    }
466
467    /**
468     * Check if the given config strings are set
469     *
470     * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
471     *
472     * @param   string[] $keys
473     * @return  bool
474     */
475    protected function _chkcnf($keys) {
476        foreach($keys as $key) {
477            if(!$this->getConf($key)) return false;
478        }
479
480        return true;
481    }
482
483    /**
484     * create an approximation of the SQL string with parameters replaced
485     *
486     * @param string $sql
487     * @param array $params
488     * @param bool $htmlescape Should the result be escaped for output in HTML?
489     * @return string
490     */
491    protected function _debugSQL($sql, $params, $htmlescape=true) {
492        foreach($params as $key => $val) {
493            if(is_int($val)) {
494                $val = $this->pdo->quote($val, PDO::PARAM_INT);
495            } elseif(is_bool($val)) {
496                $val = $this->pdo->quote($val, PDO::PARAM_BOOL);
497            } elseif(is_null($val)) {
498                $val = 'NULL';
499            } else {
500                $val = $this->pdo->quote($val);
501            }
502            $sql = str_replace($key, $val, $sql);
503        }
504        if($htmlescape) $sql = hsc($sql);
505        return $sql;
506    }
507}
508
509// vim:ts=4:sw=4:et:
510