diff --git a/lib/plugins/authpdo/_test/sqlite.test.php b/lib/plugins/authpdo/_test/sqlite.test.php index dd667a5d5b2f9fbd3ee2d5e1431164b27ff4ec32..70e8dde9806baf1d0492b472d2d70b24ecb93fa7 100644 --- a/lib/plugins/authpdo/_test/sqlite.test.php +++ b/lib/plugins/authpdo/_test/sqlite.test.php @@ -1,5 +1,21 @@ <?php + +class testable_auth_plugin_authpdo extends auth_plugin_authpdo { + public function getPluginName() { + return 'authpdo'; + } + + public function _selectGroups() { + return parent::_selectGroups(); + } + + public function _insertGroup($group) { + return parent::_insertGroup($group); + } + +} + /** * General tests for the authpdo plugin * @@ -18,14 +34,16 @@ class sqlite_plugin_authpdo_test extends DokuWikiTest { global $conf; $conf['plugin']['authpdo']['debug'] = 1; - $conf['plugin']['authpdo']['dsn'] = 'sqlite:' . $this->dbfile; + $conf['plugin']['authpdo']['dsn'] = 'sqlite:' . $this->dbfile; $conf['plugin']['authpdo']['user'] = ''; $conf['plugin']['authpdo']['pass'] = ''; - - $conf['plugin']['authpdo']['select-user'] = 'SELECT id as uid, login as user, name, pass as clear, mail FROM user WHERE login = :user'; + $conf['plugin']['authpdo']['select-user'] = 'SELECT id AS uid, login AS user, name, pass AS clear, mail FROM user WHERE login = :user'; $conf['plugin']['authpdo']['select-user-groups'] = 'SELECT * FROM member AS m, "group" AS g WHERE m.gid = g.id AND m.uid = :uid'; - + $conf['plugin']['authpdo']['select-groups'] = 'SELECT id AS gid, "group" FROM "group"'; + $conf['plugin']['authpdo']['insert-user'] = 'INSERT INTO user (login, pass, name, mail) VALUES (:user, :hash, :name, :mail)'; + $conf['plugin']['authpdo']['insert-group'] = 'INSERT INTO "group" ("group") VALUES (:group)'; + $conf['plugin']['authpdo']['join-group'] = 'INSERT INTO member (uid, gid) VALUES (:uid, :gid)'; } public function tearDown() { @@ -33,6 +51,22 @@ class sqlite_plugin_authpdo_test extends DokuWikiTest { unlink($this->dbfile); } + public function test_internals() { + $auth = new testable_auth_plugin_authpdo(); + + $groups = $auth->_selectGroups(); + $this->assertArrayHasKey('user', $groups); + $this->assertEquals(1, $groups['user']['gid']); + $this->assertArrayHasKey('admin', $groups); + $this->assertEquals(2, $groups['admin']['gid']); + + $ok = $auth->_insertGroup('test'); + $this->assertTrue($ok); + $groups = $auth->_selectGroups(); + $this->assertArrayHasKey('test', $groups); + $this->assertEquals(3, $groups['test']['gid']); + } + public function test_userinfo() { global $conf; $auth = new auth_plugin_authpdo(); @@ -43,7 +77,7 @@ class sqlite_plugin_authpdo_test extends DokuWikiTest { $this->assertTrue($auth->checkPass('admin', 'password')); // now with a hashed password - $conf['plugin']['authpdo']['select-user'] = 'SELECT id as uid, login as user, name, pass as hash, mail FROM user WHERE login = :user'; + $conf['plugin']['authpdo']['select-user'] = 'SELECT id AS uid, login AS user, name, pass AS hash, mail FROM user WHERE login = :user'; $this->assertFalse($auth->checkPass('admin', 'password')); $this->assertFalse($auth->checkPass('user', md5('password'))); @@ -52,6 +86,21 @@ class sqlite_plugin_authpdo_test extends DokuWikiTest { $this->assertEquals('admin', $info['user']); $this->assertEquals('The Admin', $info['name']); $this->assertEquals('admin@example.com', $info['mail']); - $this->assertEquals(array('admin','user'), $info['grps']); + $this->assertEquals(array('admin', 'user'), $info['grps']); + + // group retrieval + $this->assertEquals(array('admin', 'user'), $auth->retrieveGroups()); + $this->assertEquals(array('user'), $auth->retrieveGroups(1)); + $this->assertEquals(array('admin'), $auth->retrieveGroups(0, 1)); + + // user creation + $auth->createUser('test', 'password', 'A Test user', 'test@example.com', array('newgroup')); + $info = $auth->getUserData('test'); + $this->assertEquals('test', $info['user']); + $this->assertEquals('A Test user', $info['name']); + $this->assertEquals('test@example.com', $info['mail']); + $this->assertEquals(array('newgroup', 'user'), $info['grps']); + $this->assertEquals(array('admin', 'newgroup', 'user'), $auth->retrieveGroups()); } + } diff --git a/lib/plugins/authpdo/auth.php b/lib/plugins/authpdo/auth.php index 26e7f0d98b1bdf4bfad5ff2b60ed468f40e1433f..3bd48737104d590823a49d9db8d01e81c0592b9b 100644 --- a/lib/plugins/authpdo/auth.php +++ b/lib/plugins/authpdo/auth.php @@ -40,6 +40,7 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { array( PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes ) ); } catch(PDOException $e) { @@ -113,12 +114,12 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { if($requireGroups) { $data['grps'] = $this->_selectUserGroups($data); + if($data['grps'] === false) return false; } return $data; } - /** * Create a new User [implement only where required/possible] * @@ -131,16 +132,61 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { * Set addUser capability when implemented * * @param string $user - * @param string $pass + * @param string $clear * @param string $name * @param string $mail * @param null|array $grps * @return bool|null */ - //public function createUser($user, $pass, $name, $mail, $grps = null) { - // FIXME implement - // return null; - //} + public function createUser($user, $clear, $name, $mail, $grps = null) { + global $conf; + + if(($info = $this->getUserData($user, false)) !== false) { + msg($this->getLang('userexists'), -1); + return false; // user already exists + } + + // prepare data + if($grps == null) $grps = array(); + $grps[] = $conf['defaultgroup']; + $grps = array_unique($grps); + $hash = auth_cryptPassword($clear); + $userdata = compact('user', 'clear', 'hash', 'name', 'mail'); + + // action protected by transaction + $this->pdo->beginTransaction(); + { + // insert the user + $ok = $this->_query($this->getConf('insert-user'), $userdata); + if($ok === false) goto FAIL; + $userdata = $this->getUserData($user, false); + if($userdata === false) goto FAIL; + + // create all groups that do not exist, the refetch the groups + $allgroups = $this->_selectGroups(); + foreach($grps as $group) { + if(!isset($allgroups[$group])) { + $ok = $this->_insertGroup($group); + if($ok === false) goto FAIL; + } + } + $allgroups = $this->_selectGroups(); + + // add user to the groups + foreach($grps as $group) { + $ok = $this->_joinGroup($userdata, $allgroups[$group]); + if($ok === false) goto FAIL; + } + } + $this->pdo->commit(); + return true; + + // something went wrong, rollback + FAIL: + $this->pdo->rollBack(); + $this->_debug('Transaction rolled back', 0, __LINE__); + return null; // return error + } /** * Modify user data [implement only where required/possible] @@ -212,7 +258,7 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { //} /** - * Retrieve groups [implement only where required/possible] + * Retrieve groups * * Set getGroups capability when implemented * @@ -220,95 +266,27 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { * @param int $limit * @return array */ - //public function retrieveGroups($start = 0, $limit = 0) { - // FIXME implement - // return array(); - //} - - /** - * Return case sensitivity of the backend - * - * When your backend is caseinsensitive (eg. you can login with USER and - * user) then you need to overwrite this method and return false - * - * @return bool - */ - public function isCaseSensitive() { - return true; - } - - /** - * Sanitize a given username - * - * This function is applied to any user name that is given to - * the backend and should also be applied to any user name within - * the backend before returning it somewhere. - * - * This should be used to enforce username restrictions. - * - * @param string $user username - * @return string the cleaned username - */ - public function cleanUser($user) { - return $user; - } + public function retrieveGroups($start = 0, $limit = 0) { + $groups = array_keys($this->_selectGroups()); + if($groups === false) return array(); - /** - * Sanitize a given groupname - * - * This function is applied to any groupname that is given to - * the backend and should also be applied to any groupname within - * the backend before returning it somewhere. - * - * This should be used to enforce groupname restrictions. - * - * Groupnames are to be passed without a leading '@' here. - * - * @param string $group groupname - * @return string the cleaned groupname - */ - public function cleanGroup($group) { - return $group; + if(!$limit) { + return array_splice($groups, $start); + } else { + return array_splice($groups, $start, $limit); + } } - /** - * Check Session Cache validity [implement only where required/possible] - * - * DokuWiki caches user info in the user's session for the timespan defined - * in $conf['auth_security_timeout']. - * - * This makes sure slow authentication backends do not slow down DokuWiki. - * This also means that changes to the user database will not be reflected - * on currently logged in users. - * - * To accommodate for this, the user manager plugin will touch a reference - * file whenever a change is submitted. This function compares the filetime - * of this reference file with the time stored in the session. - * - * This reference file mechanism does not reflect changes done directly in - * the backend's database through other means than the user manager plugin. - * - * Fast backends might want to return always false, to force rechecks on - * each page load. Others might want to use their own checking here. If - * unsure, do not override. - * - * @param string $user - The username - * @return bool - */ - //public function useSessionCache($user) { - // FIXME implement - //} - /** * Select data of a specified user * - * @param $user - * @return bool|array + * @param string $user the user name + * @return bool|array user data, false on error */ protected function _selectUser($user) { $sql = $this->getConf('select-user'); - $result = $this->query($sql, array(':user' => $user)); + $result = $this->_query($sql, array(':user' => $user)); if(!$result) return false; if(count($result) > 1) { @@ -344,17 +322,20 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { * Select all groups of a user * * @param array $userdata The userdata as returned by _selectUser() - * @return array + * @return array|bool list of group names, false on error */ protected function _selectUserGroups($userdata) { global $conf; $sql = $this->getConf('select-user-groups'); - - $result = $this->query($sql, $userdata); + $result = $this->_query($sql, $userdata); + if($result === false) return false; $groups = array($conf['defaultgroup']); // always add default config - if($result) foreach($result as $row) { - if(!isset($row['group'])) continue; + foreach($result as $row) { + if(!isset($row['group'])) { + $this->_debug("No 'group' field returned in select-user-groups statement"); + return false; + } $groups[] = $row['group']; } @@ -363,14 +344,75 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { return $groups; } + /** + * Select all available groups + * + * @todo this should be cached + * @return array|bool list of all available groups and their properties + */ + protected function _selectGroups() { + $sql = $this->getConf('select-groups'); + $result = $this->_query($sql); + if($result === false) return false; + + $groups = array(); + foreach($result as $row) { + if(!isset($row['group'])) { + $this->_debug("No 'group' field returned from select-groups statement", -1, __LINE__); + return false; + } + + // relayout result with group name as key + $group = $row['group']; + $groups[$group] = $row; + } + + ksort($groups); + return $groups; + } + + /** + * Create a new group with the given name + * + * @param string $group + * @return bool + */ + protected function _insertGroup($group) { + $sql = $this->getConf('insert-group'); + + $result = $this->_query($sql, array(':group' => $group)); + if($result === false) return false; + return true; + } + + /** + * Enters the user to the group + * + * @param array $userdata all the user data + * @param array $groupdata all the group data + * @return bool + */ + protected function _joinGroup($userdata, $groupdata) { + $data = array_merge($userdata, $groupdata); + $sql = $this->getConf('join-group'); + $result = $this->_query($sql, $data); + if($result === false) return false; + return true; + } + /** * Executes a query * * @param string $sql The SQL statement to execute * @param array $arguments Named parameters to be used in the statement - * @return array|bool The result as associative array + * @return array|bool The result as associative array, false on error */ - protected function query($sql, $arguments) { + protected function _query($sql, $arguments = array()) { + if(empty($sql)) { + $this->_debug('No SQL query given', -1, __LINE__); + return false; + } + // prepare parameters - we only use those that exist in the SQL $params = array(); foreach($arguments as $key => $value) { @@ -381,24 +423,23 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { } // execute + $sth = $this->pdo->prepare($sql); try { - $sth = $this->pdo->prepare($sql); $sth->execute($params); $result = $sth->fetchAll(); - if((int) $sth->errorCode()) { - $this->_debug(join(' ',$sth->errorInfo()), -1, __LINE__); - $result = false; - } - $sth->closeCursor(); - $sth = null; - } catch(PDOException $e) { + } catch(Exception $e) { + $dsql = $this->_debugSQL($sql, $params, !defined('DOKU_UNITTEST')); $this->_debug($e); + $this->_debug("SQL: <pre>$dsql</pre>", -1, $e->getLine()); $result = false; + } finally { + $sth->closeCursor(); + $sth = null; } + return $result; } - /** * Wrapper around msg() but outputs only when debug is enabled * @@ -422,6 +463,47 @@ class auth_plugin_authpdo extends DokuWiki_Auth_Plugin { msg('authpdo: ' . $msg, $err, $line, __FILE__); } } + + /** + * Check if the given config strings are set + * + * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net> + * + * @param string[] $keys + * @return bool + */ + protected function _chkcnf($keys) { + foreach($keys as $key) { + if(!$this->getConf($key)) return false; + } + + return true; + } + + /** + * create an approximation of the SQL string with parameters replaced + * + * @param string $sql + * @param array $params + * @param bool $htmlescape Should the result be escaped for output in HTML? + * @return string + */ + protected function _debugSQL($sql, $params, $htmlescape=true) { + foreach($params as $key => $val) { + if(is_int($val)) { + $val = $this->pdo->quote($val, PDO::PARAM_INT); + } elseif(is_bool($val)) { + $val = $this->pdo->quote($val, PDO::PARAM_BOOL); + } elseif(is_null($val)) { + $val = 'NULL'; + } else { + $val = $this->pdo->quote($val); + } + $sql = str_replace($key, $val, $sql); + } + if($htmlescape) $sql = hsc($sql); + return $sql; + } } // vim:ts=4:sw=4:et: diff --git a/lib/plugins/authpdo/conf/default.php b/lib/plugins/authpdo/conf/default.php index 74a17c4eacb44a7d4736ca153b366ddfe6ddcc3f..c7bc9e70b70e412d1632288f7ea7cb2142d4fa00 100644 --- a/lib/plugins/authpdo/conf/default.php +++ b/lib/plugins/authpdo/conf/default.php @@ -21,9 +21,29 @@ $conf['pass'] = ''; $conf['select-user'] = ''; /** - * Select all the group names a user is member of + * Select all the existing group names * - * input: :user, [:uid], [*] - * return: group + * return: group, [gid], [*] */ -$conf['select-user-group'] = ''; +$conf['select-group'] = ''; + +/** + * Create a new user + * + * input: :user, :name, :mail, (:clear,:hash) + */ +$conf['insert-user'] = ''; + +/** + * Create a new group + * + * input: :group + */ +$conf['insert-group'] = ''; + +/** + * Make user join group + * + * input: :user, [:uid], group, [:gid], [*] + */ +$conf['join-group'] = '';