dockerwiki/content/lib/plugins/authpdo/auth.php

827 lines
25 KiB
PHP
Raw Normal View History

2021-10-26 13:02:53 +02:00
<?php
/**
* 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($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__);
}
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: