123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828 |
- <?php
- use dokuwiki\Utf8\Sort;
- /**
- * DokuWiki Plugin authpdo (Auth Component)
- *
- * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
- * @author Andreas Gohr <andi@splitbrain.org>
- */
- /**
- * Class auth_plugin_authpdo
- */
- class auth_plugin_authpdo extends DokuWiki_Auth_Plugin
- {
- /** @var PDO */
- protected $pdo;
- /** @var null|array The list of all groups */
- protected $groupcache = null;
- /**
- * Constructor.
- */
- public function __construct()
- {
- parent::__construct(); // for compatibility
- if (!class_exists('PDO')) {
- $this->debugMsg('PDO extension for PHP not found.', -1, __LINE__);
- $this->success = false;
- return;
- }
- if (!$this->getConf('dsn')) {
- $this->debugMsg('No DSN specified', -1, __LINE__);
- $this->success = false;
- return;
- }
- try {
- $this->pdo = new PDO(
- $this->getConf('dsn'),
- $this->getConf('user'),
- conf_decodeString($this->getConf('pass')),
- 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) {
- $this->debugMsg($e);
- msg($this->getLang('connectfail'), -1);
- $this->success = false;
- return;
- }
- // can Users be created?
- $this->cando['addUser'] = $this->checkConfig(
- array(
- 'select-user',
- 'select-user-groups',
- 'select-groups',
- 'insert-user',
- 'insert-group',
- 'join-group'
- )
- );
- // can Users be deleted?
- $this->cando['delUser'] = $this->checkConfig(
- array(
- 'select-user',
- 'select-user-groups',
- 'select-groups',
- 'leave-group',
- 'delete-user'
- )
- );
- // can login names be changed?
- $this->cando['modLogin'] = $this->checkConfig(
- array(
- 'select-user',
- 'select-user-groups',
- 'update-user-login'
- )
- );
- // can passwords be changed?
- $this->cando['modPass'] = $this->checkConfig(
- array(
- 'select-user',
- 'select-user-groups',
- 'update-user-pass'
- )
- );
- // can real names be changed?
- $this->cando['modName'] = $this->checkConfig(
- array(
- 'select-user',
- 'select-user-groups',
- 'update-user-info:name'
- )
- );
- // can real email be changed?
- $this->cando['modMail'] = $this->checkConfig(
- array(
- 'select-user',
- 'select-user-groups',
- 'update-user-info:mail'
- )
- );
- // can groups be changed?
- $this->cando['modGroups'] = $this->checkConfig(
- array(
- 'select-user',
- 'select-user-groups',
- 'select-groups',
- 'leave-group',
- 'join-group',
- 'insert-group'
- )
- );
- // can a filtered list of users be retrieved?
- $this->cando['getUsers'] = $this->checkConfig(
- array(
- 'list-users'
- )
- );
- // can the number of users be retrieved?
- $this->cando['getUserCount'] = $this->checkConfig(
- array(
- 'count-users'
- )
- );
- // can a list of available groups be retrieved?
- $this->cando['getGroups'] = $this->checkConfig(
- array(
- 'select-groups'
- )
- );
- $this->success = true;
- }
- /**
- * Check user+password
- *
- * @param string $user the user name
- * @param string $pass the clear text password
- * @return bool
- */
- public function checkPass($user, $pass)
- {
- $userdata = $this->selectUser($user);
- if ($userdata == false) return false;
- // password checking done in SQL?
- if ($this->checkConfig(array('check-pass'))) {
- $userdata['clear'] = $pass;
- $userdata['hash'] = auth_cryptPassword($pass);
- $result = $this->query($this->getConf('check-pass'), $userdata);
- if ($result === false) return false;
- return (count($result) == 1);
- }
- // we do password checking on our own
- if (isset($userdata['hash'])) {
- // hashed password
- $passhash = new \dokuwiki\PassHash();
- return $passhash->verify_hash($pass, $userdata['hash']);
- } else {
- // clear text password in the database O_o
- return ($pass === $userdata['clear']);
- }
- }
- /**
- * Return user info
- *
- * Returns info about the given user needs to contain
- * at least these fields:
- *
- * name string full name of the user
- * mail string email addres of the user
- * grps array list of groups the user is in
- *
- * @param string $user the user name
- * @param bool $requireGroups whether or not the returned data must include groups
- * @return array|bool containing user data or false
- */
- public function getUserData($user, $requireGroups = true)
- {
- $data = $this->selectUser($user);
- if ($data == false) return false;
- if (isset($data['hash'])) unset($data['hash']);
- if (isset($data['clean'])) unset($data['clean']);
- if ($requireGroups) {
- $data['grps'] = $this->selectUserGroups($data);
- if ($data['grps'] === false) return false;
- }
- return $data;
- }
- /**
- * Create a new User [implement only where required/possible]
- *
- * Returns false if the user already exists, null when an error
- * occurred and true if everything went well.
- *
- * The new user HAS TO be added to the default group by this
- * function!
- *
- * Set addUser capability when implemented
- *
- * @param string $user
- * @param string $clear
- * @param string $name
- * @param string $mail
- * @param null|array $grps
- * @return bool|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();
- array_unshift($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->addGroup($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->debugMsg('Transaction rolled back', 0, __LINE__);
- msg($this->getLang('writefail'), -1);
- return null; // return error
- }
- /**
- * Modify user data
- *
- * @param string $user nick of the user to be changed
- * @param array $changes array of field/value pairs to be changed (password will be clear text)
- * @return bool
- */
- public function modifyUser($user, $changes)
- {
- // secure everything in transaction
- $this->pdo->beginTransaction();
- {
- $olddata = $this->getUserData($user);
- $oldgroups = $olddata['grps'];
- unset($olddata['grps']);
- // changing the user name?
- if (isset($changes['user'])) {
- if ($this->getUserData($changes['user'], false)) goto FAIL;
- $params = $olddata;
- $params['newlogin'] = $changes['user'];
- $ok = $this->query($this->getConf('update-user-login'), $params);
- if ($ok === false) goto FAIL;
- }
- // changing the password?
- if (isset($changes['pass'])) {
- $params = $olddata;
- $params['clear'] = $changes['pass'];
- $params['hash'] = auth_cryptPassword($changes['pass']);
- $ok = $this->query($this->getConf('update-user-pass'), $params);
- if ($ok === false) goto FAIL;
- }
- // changing info?
- if (isset($changes['mail']) || isset($changes['name'])) {
- $params = $olddata;
- if (isset($changes['mail'])) $params['mail'] = $changes['mail'];
- if (isset($changes['name'])) $params['name'] = $changes['name'];
- $ok = $this->query($this->getConf('update-user-info'), $params);
- if ($ok === false) goto FAIL;
- }
- // changing groups?
- if (isset($changes['grps'])) {
- $allgroups = $this->selectGroups();
- // remove membership for previous groups
- foreach ($oldgroups as $group) {
- if (!in_array($group, $changes['grps']) && isset($allgroups[$group])) {
- $ok = $this->leaveGroup($olddata, $allgroups[$group]);
- if ($ok === false) goto FAIL;
- }
- }
- // create all new groups that are missing
- $added = 0;
- foreach ($changes['grps'] as $group) {
- if (!isset($allgroups[$group])) {
- $ok = $this->addGroup($group);
- if ($ok === false) goto FAIL;
- $added++;
- }
- }
- // reload group info
- if ($added > 0) $allgroups = $this->selectGroups();
- // add membership for new groups
- foreach ($changes['grps'] as $group) {
- if (!in_array($group, $oldgroups)) {
- $ok = $this->joinGroup($olddata, $allgroups[$group]);
- if ($ok === false) goto FAIL;
- }
- }
- }
- }
- $this->pdo->commit();
- return true;
- // something went wrong, rollback
- FAIL:
- $this->pdo->rollBack();
- $this->debugMsg('Transaction rolled back', 0, __LINE__);
- msg($this->getLang('writefail'), -1);
- return false; // return error
- }
- /**
- * Delete one or more users
- *
- * Set delUser capability when implemented
- *
- * @param array $users
- * @return int number of users deleted
- */
- public function deleteUsers($users)
- {
- $count = 0;
- foreach ($users as $user) {
- if ($this->deleteUser($user)) $count++;
- }
- return $count;
- }
- /**
- * Bulk retrieval of user data [implement only where required/possible]
- *
- * Set getUsers capability when implemented
- *
- * @param int $start index of first user to be returned
- * @param int $limit max number of users to be returned
- * @param array $filter array of field/pattern pairs, null for no filter
- * @return array list of userinfo (refer getUserData for internal userinfo details)
- */
- public function retrieveUsers($start = 0, $limit = -1, $filter = null)
- {
- if ($limit < 0) $limit = 10000; // we don't support no limit
- if (is_null($filter)) $filter = array();
- if (isset($filter['grps'])) $filter['group'] = $filter['grps'];
- foreach (array('user', 'name', 'mail', 'group') as $key) {
- if (!isset($filter[$key])) {
- $filter[$key] = '%';
- } else {
- $filter[$key] = '%' . $filter[$key] . '%';
- }
- }
- $filter['start'] = (int)$start;
- $filter['end'] = (int)$start + $limit;
- $filter['limit'] = (int)$limit;
- $result = $this->query($this->getConf('list-users'), $filter);
- if (!$result) return array();
- $users = array();
- if (is_array($result)) {
- foreach ($result as $row) {
- if (!isset($row['user'])) {
- $this->debugMsg("list-users statement did not return 'user' attribute", -1, __LINE__);
- return array();
- }
- $users[] = $this->getUserData($row['user']);
- }
- } else {
- $this->debugMsg("list-users statement did not return a list of result", -1, __LINE__);
- }
- return $users;
- }
- /**
- * Return a count of the number of user which meet $filter criteria
- *
- * @param array $filter array of field/pattern pairs, empty array for no filter
- * @return int
- */
- public function getUserCount($filter = array())
- {
- if (is_null($filter)) $filter = array();
- if (isset($filter['grps'])) $filter['group'] = $filter['grps'];
- foreach (array('user', 'name', 'mail', 'group') as $key) {
- if (!isset($filter[$key])) {
- $filter[$key] = '%';
- } else {
- $filter[$key] = '%' . $filter[$key] . '%';
- }
- }
- $result = $this->query($this->getConf('count-users'), $filter);
- if (!$result || !isset($result[0]['count'])) {
- $this->debugMsg("Statement did not return 'count' attribute", -1, __LINE__);
- }
- return (int)$result[0]['count'];
- }
- /**
- * Create a new group with the given name
- *
- * @param string $group
- * @return bool
- */
- public function addGroup($group)
- {
- $sql = $this->getConf('insert-group');
- $result = $this->query($sql, array(':group' => $group));
- $this->clearGroupCache();
- if ($result === false) return false;
- return true;
- }
- /**
- * Retrieve groups
- *
- * Set getGroups capability when implemented
- *
- * @param int $start
- * @param int $limit
- * @return array
- */
- public function retrieveGroups($start = 0, $limit = 0)
- {
- $groups = array_keys($this->selectGroups());
- if ($groups === false) return array();
- if (!$limit) {
- return array_splice($groups, $start);
- } else {
- return array_splice($groups, $start, $limit);
- }
- }
- /**
- * Select data of a specified user
- *
- * @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));
- if (!$result) return false;
- if (count($result) > 1) {
- $this->debugMsg('Found more than one matching user', -1, __LINE__);
- return false;
- }
- $data = array_shift($result);
- $dataok = true;
- if (!isset($data['user'])) {
- $this->debugMsg("Statement did not return 'user' attribute", -1, __LINE__);
- $dataok = false;
- }
- if (!isset($data['hash']) && !isset($data['clear']) && !$this->checkConfig(array('check-pass'))) {
- $this->debugMsg("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__);
- $dataok = false;
- }
- if (!isset($data['name'])) {
- $this->debugMsg("Statement did not return 'name' attribute", -1, __LINE__);
- $dataok = false;
- }
- if (!isset($data['mail'])) {
- $this->debugMsg("Statement did not return 'mail' attribute", -1, __LINE__);
- $dataok = false;
- }
- if (!$dataok) return false;
- return $data;
- }
- /**
- * Delete a user after removing all their group memberships
- *
- * @param string $user
- * @return bool true when the user was deleted
- */
- protected function deleteUser($user)
- {
- $this->pdo->beginTransaction();
- {
- $userdata = $this->getUserData($user);
- if ($userdata === false) goto FAIL;
- $allgroups = $this->selectGroups();
- // remove group memberships (ignore errors)
- foreach ($userdata['grps'] as $group) {
- if (isset($allgroups[$group])) {
- $this->leaveGroup($userdata, $allgroups[$group]);
- }
- }
- $ok = $this->query($this->getConf('delete-user'), $userdata);
- if ($ok === false) goto FAIL;
- }
- $this->pdo->commit();
- return true;
- FAIL:
- $this->pdo->rollBack();
- return false;
- }
- /**
- * Select all groups of a user
- *
- * @param array $userdata The userdata as returned by _selectUser()
- * @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);
- if ($result === false) return false;
- $groups = array($conf['defaultgroup']); // always add default config
- if (is_array($result)) {
- foreach ($result as $row) {
- if (!isset($row['group'])) {
- $this->debugMsg("No 'group' field returned in select-user-groups statement", -1, __LINE__);
- return false;
- }
- $groups[] = $row['group'];
- }
- } else {
- $this->debugMsg("select-user-groups statement did not return a list of result", -1, __LINE__);
- }
- $groups = array_unique($groups);
- Sort::sort($groups);
- return $groups;
- }
- /**
- * Select all available groups
- *
- * @return array|bool list of all available groups and their properties
- */
- protected function selectGroups()
- {
- if ($this->groupcache) return $this->groupcache;
- $sql = $this->getConf('select-groups');
- $result = $this->query($sql);
- if ($result === false) return false;
- $groups = array();
- if (is_array($result)) {
- foreach ($result as $row) {
- if (!isset($row['group'])) {
- $this->debugMsg("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;
- }
- } else {
- $this->debugMsg("select-groups statement did not return a list of result", -1, __LINE__);
- }
- Sort::ksort($groups);
- return $groups;
- }
- /**
- * Remove all entries from the group cache
- */
- protected function clearGroupCache()
- {
- $this->groupcache = null;
- }
- /**
- * Adds 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;
- }
- /**
- * Removes the user from the group
- *
- * @param array $userdata all the user data
- * @param array $groupdata all the group data
- * @return bool
- */
- protected function leaveGroup($userdata, $groupdata)
- {
- $data = array_merge($userdata, $groupdata);
- $sql = $this->getConf('leave-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|int|bool The result as associative array for SELECTs, affected rows for others, false on error
- */
- protected function query($sql, $arguments = array())
- {
- $sql = trim($sql);
- if (empty($sql)) {
- $this->debugMsg('No SQL query given', -1, __LINE__);
- return false;
- }
- // execute
- $params = array();
- $sth = $this->pdo->prepare($sql);
- $result = false;
- try {
- // prepare parameters - we only use those that exist in the SQL
- foreach ($arguments as $key => $value) {
- if (is_array($value)) continue;
- if (is_object($value)) continue;
- if ($key[0] != ':') $key = ":$key"; // prefix with colon if needed
- if (strpos($sql, $key) === false) continue; // skip if parameter is missing
- if (is_int($value)) {
- $sth->bindValue($key, $value, PDO::PARAM_INT);
- } else {
- $sth->bindValue($key, $value);
- }
- $params[$key] = $value; //remember for debugging
- }
- $sth->execute();
- // only report last line's result
- $hasnextrowset = true;
- $currentsql = $sql;
- while ($hasnextrowset) {
- if (strtolower(substr($currentsql, 0, 6)) == 'select') {
- $result = $sth->fetchAll();
- } else {
- $result = $sth->rowCount();
- }
- $semi_pos = strpos($currentsql, ';');
- if ($semi_pos) {
- $currentsql = trim(substr($currentsql, $semi_pos + 1));
- }
- try {
- $hasnextrowset = $sth->nextRowset(); // run next rowset
- } catch (PDOException $rowset_e) {
- $hasnextrowset = false; // driver does not support multi-rowset, should be executed in one time
- }
- }
- } catch (Exception $e) {
- // report the caller's line
- $trace = debug_backtrace();
- $line = $trace[0]['line'];
- $dsql = $this->debugSQL($sql, $params, !defined('DOKU_UNITTEST'));
- $this->debugMsg($e, -1, $line);
- $this->debugMsg("SQL: <pre>$dsql</pre>", -1, $line);
- }
- $sth->closeCursor();
- $sth = null;
- return $result;
- }
- /**
- * Wrapper around msg() but outputs only when debug is enabled
- *
- * @param string|Exception $message
- * @param int $err
- * @param int $line
- */
- protected function debugMsg($message, $err = 0, $line = 0)
- {
- if (!$this->getConf('debug')) return;
- if (is_a($message, 'Exception')) {
- $err = -1;
- $msg = $message->getMessage();
- if (!$line) $line = $message->getLine();
- } else {
- $msg = $message;
- }
- if (defined('DOKU_UNITTEST')) {
- printf("\n%s, %s:%d\n", $msg, __FILE__, $line);
- } else {
- msg('authpdo: ' . $msg, $err, $line, __FILE__);
- }
- }
- /**
- * Check if the given config strings are set
- *
- * @param string[] $keys
- * @return bool
- * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
- *
- */
- protected function checkConfig($keys)
- {
- foreach ($keys as $key) {
- $params = explode(':', $key);
- $key = array_shift($params);
- $sql = trim($this->getConf($key));
- // check if sql is set
- if (!$sql) return false;
- // check if needed params are there
- foreach ($params as $param) {
- if (strpos($sql, ":$param") === false) 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:
|