xref: /dokuwiki/lib/plugins/authpdo/auth.php (revision 70a89417b85aed070861be4f936ffa8844eb63dd)
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                )
44            );
45        } catch(PDOException $e) {
46            $this->_debug($e);
47            $this->success = false;
48            return;
49        }
50
51        // FIXME set capabilities accordingly
52        //$this->cando['addUser']     = false; // can Users be created?
53        //$this->cando['delUser']     = false; // can Users be deleted?
54        //$this->cando['modLogin']    = false; // can login names be changed?
55        //$this->cando['modPass']     = false; // can passwords be changed?
56        //$this->cando['modName']     = false; // can real names be changed?
57        //$this->cando['modMail']     = false; // can emails be changed?
58        //$this->cando['modGroups']   = false; // can groups be changed?
59        //$this->cando['getUsers']    = false; // can a (filtered) list of users be retrieved?
60        //$this->cando['getUserCount']= false; // can the number of users be retrieved?
61        //$this->cando['getGroups']   = false; // can a list of available groups be retrieved?
62        //$this->cando['external']    = false; // does the module do external auth checking?
63        //$this->cando['logout']      = true; // can the user logout again? (eg. not possible with HTTP auth)
64
65        // FIXME intialize your auth system and set success to true, if successful
66        $this->success = true;
67    }
68
69    /**
70     * Check user+password
71     *
72     * May be ommited if trustExternal is used.
73     *
74     * @param   string $user the user name
75     * @param   string $pass the clear text password
76     * @return  bool
77     */
78    public function checkPass($user, $pass) {
79
80        $data = $this->_selectUser($user);
81        if($data == false) return false;
82
83        if(isset($data['hash'])) {
84            // hashed password
85            $passhash = new PassHash();
86            return $passhash->verify_hash($pass, $data['hash']);
87        } else {
88            // clear text password in the database O_o
89            return ($pass == $data['clear']);
90        }
91    }
92
93    /**
94     * Return user info
95     *
96     * Returns info about the given user needs to contain
97     * at least these fields:
98     *
99     * name string  full name of the user
100     * mail string  email addres of the user
101     * grps array   list of groups the user is in
102     *
103     * @param   string $user the user name
104     * @param   bool $requireGroups whether or not the returned data must include groups
105     * @return array containing user data or false
106     */
107    public function getUserData($user, $requireGroups = true) {
108        $data = $this->_selectUser($user);
109        if($data == false) return false;
110
111        if(isset($data['hash'])) unset($data['hash']);
112        if(isset($data['clean'])) unset($data['clean']);
113
114        if($requireGroups) {
115            $data['grps'] = $this->_selectUserGroups($data);
116        }
117
118        return $data;
119    }
120
121
122    /**
123     * Create a new User [implement only where required/possible]
124     *
125     * Returns false if the user already exists, null when an error
126     * occurred and true if everything went well.
127     *
128     * The new user HAS TO be added to the default group by this
129     * function!
130     *
131     * Set addUser capability when implemented
132     *
133     * @param  string $user
134     * @param  string $pass
135     * @param  string $name
136     * @param  string $mail
137     * @param  null|array $grps
138     * @return bool|null
139     */
140    //public function createUser($user, $pass, $name, $mail, $grps = null) {
141    // FIXME implement
142    //    return null;
143    //}
144
145    /**
146     * Modify user data [implement only where required/possible]
147     *
148     * Set the mod* capabilities according to the implemented features
149     *
150     * @param   string $user nick of the user to be changed
151     * @param   array $changes array of field/value pairs to be changed (password will be clear text)
152     * @return  bool
153     */
154    //public function modifyUser($user, $changes) {
155    // FIXME implement
156    //    return false;
157    //}
158
159    /**
160     * Delete one or more users [implement only where required/possible]
161     *
162     * Set delUser capability when implemented
163     *
164     * @param   array $users
165     * @return  int    number of users deleted
166     */
167    //public function deleteUsers($users) {
168    // FIXME implement
169    //    return false;
170    //}
171
172    /**
173     * Bulk retrieval of user data [implement only where required/possible]
174     *
175     * Set getUsers capability when implemented
176     *
177     * @param   int $start index of first user to be returned
178     * @param   int $limit max number of users to be returned
179     * @param   array $filter array of field/pattern pairs, null for no filter
180     * @return  array list of userinfo (refer getUserData for internal userinfo details)
181     */
182    //public function retrieveUsers($start = 0, $limit = -1, $filter = null) {
183    // FIXME implement
184    //    return array();
185    //}
186
187    /**
188     * Return a count of the number of user which meet $filter criteria
189     * [should be implemented whenever retrieveUsers is implemented]
190     *
191     * Set getUserCount capability when implemented
192     *
193     * @param  array $filter array of field/pattern pairs, empty array for no filter
194     * @return int
195     */
196    //public function getUserCount($filter = array()) {
197    // FIXME implement
198    //    return 0;
199    //}
200
201    /**
202     * Define a group [implement only where required/possible]
203     *
204     * Set addGroup capability when implemented
205     *
206     * @param   string $group
207     * @return  bool
208     */
209    //public function addGroup($group) {
210    // FIXME implement
211    //    return false;
212    //}
213
214    /**
215     * Retrieve groups [implement only where required/possible]
216     *
217     * Set getGroups capability when implemented
218     *
219     * @param   int $start
220     * @param   int $limit
221     * @return  array
222     */
223    //public function retrieveGroups($start = 0, $limit = 0) {
224    // FIXME implement
225    //    return array();
226    //}
227
228    /**
229     * Return case sensitivity of the backend
230     *
231     * When your backend is caseinsensitive (eg. you can login with USER and
232     * user) then you need to overwrite this method and return false
233     *
234     * @return bool
235     */
236    public function isCaseSensitive() {
237        return true;
238    }
239
240    /**
241     * Sanitize a given username
242     *
243     * This function is applied to any user name that is given to
244     * the backend and should also be applied to any user name within
245     * the backend before returning it somewhere.
246     *
247     * This should be used to enforce username restrictions.
248     *
249     * @param string $user username
250     * @return string the cleaned username
251     */
252    public function cleanUser($user) {
253        return $user;
254    }
255
256    /**
257     * Sanitize a given groupname
258     *
259     * This function is applied to any groupname that is given to
260     * the backend and should also be applied to any groupname within
261     * the backend before returning it somewhere.
262     *
263     * This should be used to enforce groupname restrictions.
264     *
265     * Groupnames are to be passed without a leading '@' here.
266     *
267     * @param  string $group groupname
268     * @return string the cleaned groupname
269     */
270    public function cleanGroup($group) {
271        return $group;
272    }
273
274    /**
275     * Check Session Cache validity [implement only where required/possible]
276     *
277     * DokuWiki caches user info in the user's session for the timespan defined
278     * in $conf['auth_security_timeout'].
279     *
280     * This makes sure slow authentication backends do not slow down DokuWiki.
281     * This also means that changes to the user database will not be reflected
282     * on currently logged in users.
283     *
284     * To accommodate for this, the user manager plugin will touch a reference
285     * file whenever a change is submitted. This function compares the filetime
286     * of this reference file with the time stored in the session.
287     *
288     * This reference file mechanism does not reflect changes done directly in
289     * the backend's database through other means than the user manager plugin.
290     *
291     * Fast backends might want to return always false, to force rechecks on
292     * each page load. Others might want to use their own checking here. If
293     * unsure, do not override.
294     *
295     * @param  string $user - The username
296     * @return bool
297     */
298    //public function useSessionCache($user) {
299    // FIXME implement
300    //}
301
302    /**
303     * Select data of a specified user
304     *
305     * @param $user
306     * @return bool|array
307     */
308    protected function _selectUser($user) {
309        $sql = $this->getConf('select-user');
310
311        $result = $this->query($sql, array(':user' => $user));
312        if(!$result) return false;
313
314        if(count($result) > 1) {
315            $this->_debug('Found more than one matching user', -1, __LINE__);
316            return false;
317        }
318
319        $data = array_shift($result);
320        $dataok = true;
321
322        if(!isset($data['user'])) {
323            $this->_debug("Statement did not return 'user' attribute", -1, __LINE__);
324            $dataok = false;
325        }
326        if(!isset($data['hash']) && !isset($data['clear'])) {
327            $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__);
328            $dataok = false;
329        }
330        if(!isset($data['name'])) {
331            $this->_debug("Statement did not return 'name' attribute", -1, __LINE__);
332            $dataok = false;
333        }
334        if(!isset($data['mail'])) {
335            $this->_debug("Statement did not return 'mail' attribute", -1, __LINE__);
336            $dataok = false;
337        }
338
339        if(!$dataok) return false;
340        return $data;
341    }
342
343    /**
344     * Select all groups of a user
345     *
346     * @param array $userdata The userdata as returned by _selectUser()
347     * @return array
348     */
349    protected function _selectUserGroups($userdata) {
350        global $conf;
351        $sql = $this->getConf('select-user-groups');
352
353        $result = $this->query($sql, $userdata);
354
355        $groups = array($conf['defaultgroup']); // always add default config
356        if($result) foreach($result as $row) {
357            if(!isset($row['group'])) continue;
358            $groups[] = $row['group'];
359        }
360
361        $groups = array_unique($groups);
362        sort($groups);
363        return $groups;
364    }
365
366    /**
367     * Executes a query
368     *
369     * @param string $sql The SQL statement to execute
370     * @param array $arguments Named parameters to be used in the statement
371     * @return array|bool The result as associative array
372     */
373    protected function query($sql, $arguments) {
374        // prepare parameters - we only use those that exist in the SQL
375        $params = array();
376        foreach($arguments as $key => $value) {
377            if(is_array($value)) continue;
378            if(is_object($value)) continue;
379            if($key[0] != ':') $key = ":$key"; // prefix with colon if needed
380            if(strpos($sql, $key) !== false) $params[$key] = $value;
381        }
382
383        // execute
384        try {
385            $sth = $this->pdo->prepare($sql);
386            $sth->execute($params);
387            $result = $sth->fetchAll();
388            if((int) $sth->errorCode()) {
389                $this->_debug(join(' ',$sth->errorInfo()), -1, __LINE__);
390                $result = false;
391            }
392            $sth->closeCursor();
393            $sth = null;
394        } catch(PDOException $e) {
395            $this->_debug($e);
396            $result = false;
397        }
398        return $result;
399    }
400
401
402    /**
403     * Wrapper around msg() but outputs only when debug is enabled
404     *
405     * @param string|Exception $message
406     * @param int $err
407     * @param int $line
408     */
409    protected function _debug($message, $err = 0, $line = 0) {
410        if(!$this->getConf('debug')) return;
411        if(is_a($message, 'Exception')) {
412            $err = -1;
413            $line = $message->getLine();
414            $msg = $message->getMessage();
415        } else {
416            $msg = $message;
417        }
418
419        if(defined('DOKU_UNITTEST')) {
420            printf("\n%s, %s:%d\n", $msg, __FILE__, $line);
421        } else {
422            msg('authpdo: ' . $msg, $err, $line, __FILE__);
423        }
424    }
425}
426
427// vim:ts=4:sw=4:et:
428