about summary refs log tree commit diff stats
path: root/wiki/lib/plugins/authpdo
diff options
context:
space:
mode:
Diffstat (limited to 'wiki/lib/plugins/authpdo')
-rw-r--r--wiki/lib/plugins/authpdo/README27
-rw-r--r--wiki/lib/plugins/authpdo/auth.php781
-rw-r--r--wiki/lib/plugins/authpdo/conf/default.php118
-rw-r--r--wiki/lib/plugins/authpdo/conf/metadata.php27
-rw-r--r--wiki/lib/plugins/authpdo/lang/bg/lang.php9
-rw-r--r--wiki/lib/plugins/authpdo/lang/cs/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/cy/lang.php12
-rw-r--r--wiki/lib/plugins/authpdo/lang/de/lang.php12
-rw-r--r--wiki/lib/plugins/authpdo/lang/en/lang.php12
-rw-r--r--wiki/lib/plugins/authpdo/lang/en/settings.php25
-rw-r--r--wiki/lib/plugins/authpdo/lang/es/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/fa/lang.php11
-rw-r--r--wiki/lib/plugins/authpdo/lang/fr/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/hr/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/hu/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/it/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/ja/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/ko/lang.php11
-rw-r--r--wiki/lib/plugins/authpdo/lang/nl/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/pt-br/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/pt/lang.php9
-rw-r--r--wiki/lib/plugins/authpdo/lang/ru/lang.php11
-rw-r--r--wiki/lib/plugins/authpdo/lang/sk/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/lang/tr/lang.php8
-rw-r--r--wiki/lib/plugins/authpdo/lang/zh/lang.php10
-rw-r--r--wiki/lib/plugins/authpdo/plugin.info.txt7
26 files changed, 1190 insertions, 0 deletions
diff --git a/wiki/lib/plugins/authpdo/README b/wiki/lib/plugins/authpdo/README
new file mode 100644
index 0000000..c99bfbf
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/README
@@ -0,0 +1,27 @@
+authpdo Plugin for DokuWiki
+
+Authenticate against a database via PDO
+
+All documentation for this plugin can be found at
+https://www.dokuwiki.org/plugin:authpdo
+
+If you install this plugin manually, make sure it is installed in
+lib/plugins/authpdo/ - if the folder is called different it
+will not work!
+
+Please refer to http://www.dokuwiki.org/plugins for additional info
+on how to install plugins in DokuWiki.
+
+----
+Copyright (C) Andreas Gohr <andi@splitbrain.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; version 2 of the License
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+See the COPYING file in your DokuWiki folder for details
diff --git a/wiki/lib/plugins/authpdo/auth.php b/wiki/lib/plugins/authpdo/auth.php
new file mode 100644
index 0000000..dfe1254
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/auth.php
@@ -0,0 +1,781 @@
+<?php
+/**
+ * DokuWiki Plugin authpdo (Auth Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author  Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * 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->_debug('PDO extension for PHP not found.', -1, __LINE__);
+            $this->success = false;
+            return;
+        }
+
+        if(!$this->getConf('dsn')) {
+            $this->_debug('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->_debug($e);
+            msg($this->getLang('connectfail'), -1);
+            $this->success = false;
+            return;
+        }
+
+        // can Users be created?
+        $this->cando['addUser'] = $this->_chkcnf(
+            array(
+                'select-user',
+                'select-user-groups',
+                'select-groups',
+                'insert-user',
+                'insert-group',
+                'join-group'
+            )
+        );
+
+        // can Users be deleted?
+        $this->cando['delUser'] = $this->_chkcnf(
+            array(
+                'select-user',
+                'select-user-groups',
+                'select-groups',
+                'leave-group',
+                'delete-user'
+            )
+        );
+
+        // can login names be changed?
+        $this->cando['modLogin'] = $this->_chkcnf(
+            array(
+                'select-user',
+                'select-user-groups',
+                'update-user-login'
+            )
+        );
+
+        // can passwords be changed?
+        $this->cando['modPass'] = $this->_chkcnf(
+            array(
+                'select-user',
+                'select-user-groups',
+                'update-user-pass'
+            )
+        );
+
+        // can real names be changed?
+        $this->cando['modName'] = $this->_chkcnf(
+            array(
+                'select-user',
+                'select-user-groups',
+                'update-user-info:name'
+            )
+        );
+
+        // can real email be changed?
+        $this->cando['modMail'] = $this->_chkcnf(
+            array(
+                'select-user',
+                'select-user-groups',
+                'update-user-info:mail'
+            )
+        );
+
+        // can groups be changed?
+        $this->cando['modGroups'] = $this->_chkcnf(
+            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->_chkcnf(
+            array(
+                'list-users'
+            )
+        );
+
+        // can the number of users be retrieved?
+        $this->cando['getUserCount'] = $this->_chkcnf(
+            array(
+                'count-users'
+            )
+        );
+
+        // can a list of available groups be retrieved?
+        $this->cando['getGroups'] = $this->_chkcnf(
+            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->_chkcnf(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 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->_debug('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->_debug('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();
+        foreach($result as $row) {
+            if(!isset($row['user'])) {
+                $this->_debug("Statement did not return 'user' attribute", -1, __LINE__);
+                return array();
+            }
+            $users[] = $this->getUserData($row['user']);
+        }
+        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->_debug("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->_debug('Found more than one matching user', -1, __LINE__);
+            return false;
+        }
+
+        $data = array_shift($result);
+        $dataok = true;
+
+        if(!isset($data['user'])) {
+            $this->_debug("Statement did not return 'user' attribute", -1, __LINE__);
+            $dataok = false;
+        }
+        if(!isset($data['hash']) && !isset($data['clear']) && !$this->_chkcnf(array('check-pass'))) {
+            $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__);
+            $dataok = false;
+        }
+        if(!isset($data['name'])) {
+            $this->_debug("Statement did not return 'name' attribute", -1, __LINE__);
+            $dataok = false;
+        }
+        if(!isset($data['mail'])) {
+            $this->_debug("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
+        foreach($result as $row) {
+            if(!isset($row['group'])) {
+                $this->_debug("No 'group' field returned in select-user-groups statement");
+                return false;
+            }
+            $groups[] = $row['group'];
+        }
+
+        $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();
+        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;
+    }
+
+    /**
+     * 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->_debug('No SQL query given', -1, __LINE__);
+            return false;
+        }
+
+        // execute
+        $params = array();
+        $sth = $this->pdo->prepare($sql);
+        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();
+            if(strtolower(substr($sql, 0, 6)) == 'select') {
+                $result = $sth->fetchAll();
+            } else {
+                $result = $sth->rowCount();
+            }
+        } catch(Exception $e) {
+            // report the caller's line
+            $trace = debug_backtrace();
+            $line = $trace[0]['line'];
+            $dsql = $this->_debugSQL($sql, $params, !defined('DOKU_UNITTEST'));
+            $this->_debug($e, -1, $line);
+            $this->_debug("SQL: <pre>$dsql</pre>", -1, $line);
+            $result = false;
+        }
+        $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 _debug($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
+     *
+     * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
+     *
+     * @param   string[] $keys
+     * @return  bool
+     */
+    protected function _chkcnf($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:
diff --git a/wiki/lib/plugins/authpdo/conf/default.php b/wiki/lib/plugins/authpdo/conf/default.php
new file mode 100644
index 0000000..138ca2f
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/conf/default.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Default settings for the authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$conf['debug'] = 0;
+$conf['dsn'] = '';
+$conf['user'] = '';
+$conf['pass'] = '';
+
+/**
+ * statement to select a single user identified by its login name
+ *
+ * input: :user
+ * return: user, name, mail, (clear|hash), [uid], [*]
+ */
+$conf['select-user'] = '';
+
+/**
+ * statement to check the password in SQL, optional when above returned clear or hash
+ *
+ * input: :user, :clear, :hash, [uid], [*]
+ * return: *
+ */
+$conf['check-pass'] = '';
+
+/**
+ * statement to select a single user identified by its login name
+ *
+ * input: :user, [uid]
+ * return: group
+ */
+$conf['select-user-groups'] = '';
+
+/**
+ * Select all the existing group names
+ *
+ * return: group, [gid], [*]
+ */
+$conf['select-groups'] = '';
+
+/**
+ * Create a new user
+ *
+ * input: :user, :name, :mail, (:clear|:hash)
+ */
+$conf['insert-user'] = '';
+
+/**
+ * Remove a user
+ *
+ * input: :user, [:uid], [*]
+ */
+$conf['delete-user'] = '';
+
+/**
+ * list user names matching the given criteria
+ *
+ * Make sure the list is distinct and sorted by user name. Apply the given limit and offset
+ *
+ * input: :user, :name, :mail, :group, :start, :end, :limit
+ * out: user
+ */
+$conf['list-users'] = '';
+
+/**
+ * count user names matching the given criteria
+ *
+ * Make sure the counted list is distinct
+ *
+ * input: :user, :name, :mail, :group
+ * out: count
+ */
+$conf['count-users'] = '';
+
+/**
+ * Update user data (except password and user name)
+ *
+ * input: :user, :name, :mail, [:uid], [*]
+ */
+$conf['update-user-info'] = '';
+
+/**
+ * Update user name aka login
+ *
+ * input: :user, :newlogin, [:uid], [*]
+ */
+$conf['update-user-login'] = '';
+
+/**
+ * Update user password
+ *
+ * input: :user, :clear, :hash, [:uid], [*]
+ */
+$conf['update-user-pass'] = '';
+
+/**
+ * Create a new group
+ *
+ * input: :group
+ */
+$conf['insert-group'] = '';
+
+/**
+ * Make user join group
+ *
+ * input: :user, [:uid], group, [:gid], [*]
+ */
+$conf['join-group'] = '';
+
+/**
+ * Make user leave group
+ *
+ * input: :user, [:uid], group, [:gid], [*]
+ */
+$conf['leave-group'] = '';
diff --git a/wiki/lib/plugins/authpdo/conf/metadata.php b/wiki/lib/plugins/authpdo/conf/metadata.php
new file mode 100644
index 0000000..7c2ee8c
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/conf/metadata.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Options for the authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$meta['debug']              = array('onoff', '_caution' => 'security');
+$meta['dsn']                = array('string', '_caution' => 'danger');
+$meta['user']               = array('string', '_caution' => 'danger');
+$meta['pass']               = array('password', '_caution' => 'danger', '_code' => 'base64');
+$meta['select-user']        = array('', '_caution' => 'danger');
+$meta['check-pass']         = array('', '_caution' => 'danger');
+$meta['select-user-groups'] = array('', '_caution' => 'danger');
+$meta['select-groups']      = array('', '_caution' => 'danger');
+$meta['insert-user']        = array('', '_caution' => 'danger');
+$meta['delete-user']        = array('', '_caution' => 'danger');
+$meta['list-users']         = array('', '_caution' => 'danger');
+$meta['count-users']        = array('', '_caution' => 'danger');
+$meta['update-user-info']   = array('', '_caution' => 'danger');
+$meta['update-user-login']  = array('', '_caution' => 'danger');
+$meta['update-user-pass']   = array('', '_caution' => 'danger');
+$meta['insert-group']       = array('', '_caution' => 'danger');
+$meta['join-group']         = array('', '_caution' => 'danger');
+$meta['leave-group']        = array('', '_caution' => 'danger');
+
+
diff --git a/wiki/lib/plugins/authpdo/lang/bg/lang.php b/wiki/lib/plugins/authpdo/lang/bg/lang.php
new file mode 100644
index 0000000..f6532c4
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/bg/lang.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Kiril <neohidra@gmail.com>
+ */
+$lang['connectfail']           = 'Свързването с базата данни се провали.';
+$lang['userexists']            = 'За съжаление вече съществува потребител с това име.';
diff --git a/wiki/lib/plugins/authpdo/lang/cs/lang.php b/wiki/lib/plugins/authpdo/lang/cs/lang.php
new file mode 100644
index 0000000..cf52a18
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/cs/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Jaroslav Lichtblau <jlichtblau@seznam.cz>
+ */
+$lang['connectfail']           = 'Selhalo připojení k databázi.';
+$lang['userexists']            = 'Omlouváme se, ale uživatel s tímto jménem již existuje.';
+$lang['writefail']             = 'Nelze změnit údaje uživatele. Informujte prosím správce wiki';
diff --git a/wiki/lib/plugins/authpdo/lang/cy/lang.php b/wiki/lib/plugins/authpdo/lang/cy/lang.php
new file mode 100644
index 0000000..449e3ef
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/cy/lang.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Welsh language file for authmysql plugin
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ */
+
+$lang['connectfail']    = 'Method y cysylltiad i\'r databas.';
+$lang['userexists']     = 'Sori, mae defnyddiwr gyda\'r enw mewngofnodi hwn eisoes yn bodoli.';
+$lang['writefail']      = 'Methu â newid data defnyddiwr. Rhowch wybod i Weinyddwr y Wici';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/wiki/lib/plugins/authpdo/lang/de/lang.php b/wiki/lib/plugins/authpdo/lang/de/lang.php
new file mode 100644
index 0000000..7ae13dd
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/de/lang.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Noel Tilliot <noeltilliot@byom.de>
+ * @author Hendrik Diel <diel.hendrik@gmail.com>
+ * @author Philip Knack <p.knack@stollfuss.de>
+ */
+$lang['connectfail']           = 'Verbindung zur Datenbank fehlgeschlagen.';
+$lang['userexists']            = 'Entschuldigung, aber dieser Benutzername ist bereits vergeben.';
+$lang['writefail']             = 'Die Benutzerdaten konnten nicht geändert werden. Bitte wenden Sie sich an den Wiki-Admin.';
diff --git a/wiki/lib/plugins/authpdo/lang/en/lang.php b/wiki/lib/plugins/authpdo/lang/en/lang.php
new file mode 100644
index 0000000..3e1482e
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/en/lang.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * English language file for authpdo plugin
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ */
+
+$lang['connectfail']    = 'Failed to connect to database.';
+$lang['userexists']     = 'Sorry, a user with this login already exists.';
+$lang['writefail']      = 'Unable to modify user data. Please inform the Wiki-Admin';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/wiki/lib/plugins/authpdo/lang/en/settings.php b/wiki/lib/plugins/authpdo/lang/en/settings.php
new file mode 100644
index 0000000..1aaaec0
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/en/settings.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * english language file for authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$lang['debug']              = 'Print out detailed error messages. Should be disabled after setup.';
+$lang['dsn']                = 'The DSN to connect to the database.';
+$lang['user']               = 'The user for the above database connection (empty for sqlite)';
+$lang['pass']               = 'The password for the above database connection (empty for sqlite)';
+$lang['select-user']        = 'SQL Statement to select the data of a single user';
+$lang['select-user-groups'] = 'SQL Statement to select all groups of a single user';
+$lang['select-groups']      = 'SQL Statement to select all available groups';
+$lang['insert-user']        = 'SQL Statement to insert a new user into the database';
+$lang['delete-user']        = 'SQL Statement to remove a single user from the database';
+$lang['list-users']         = 'SQL Statement to list users matching a filter';
+$lang['count-users']        = 'SQL Statement to count users matching a filter';
+$lang['update-user-info']   = 'SQL Statement to update the full name and email address of a single user';
+$lang['update-user-login']  = 'SQL Statement to update the login name of a single user';
+$lang['update-user-pass']   = 'SQL Statement to update the password of a single user';
+$lang['insert-group']       = 'SQL Statement to insert a new group into the database';
+$lang['join-group']         = 'SQL Statement to add a user to an existing group';
+$lang['leave-group']        = 'SQL Statement to remove a user from a group';
+$lang['check-pass']         = 'SQL Statement to check the password for a user. Can be left empty if password info is fetched in select-user.';
diff --git a/wiki/lib/plugins/authpdo/lang/es/lang.php b/wiki/lib/plugins/authpdo/lang/es/lang.php
new file mode 100644
index 0000000..9bd9211
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/es/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Domingo Redal <docxml@gmail.com>
+ */
+$lang['connectfail']           = 'Error al conectar con la base de datos.';
+$lang['userexists']            = 'Lo sentimos, ya existe un usuario con ese inicio de sesión.';
+$lang['writefail']             = 'No es posible modificar los datos del usuario. Por favor, informa al Administrador del Wiki';
diff --git a/wiki/lib/plugins/authpdo/lang/fa/lang.php b/wiki/lib/plugins/authpdo/lang/fa/lang.php
new file mode 100644
index 0000000..b26e836
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/fa/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Mohmmad Razavi <sepent@gmail.com>
+ * @author Masoud Sadrnezhaad <masoud@sadrnezhaad.ir>
+ */
+$lang['connectfail']           = 'خطا در اتصال به دیتابیس';
+$lang['userexists']            = 'با عرض پوزش، یک کاربر با این نام از قبل وجود دارد.';
+$lang['writefail']             = 'امکان تغییر داده کاربر وجود نداشت. لطفا مسئول Wiki را آگاه کنید.';
diff --git a/wiki/lib/plugins/authpdo/lang/fr/lang.php b/wiki/lib/plugins/authpdo/lang/fr/lang.php
new file mode 100644
index 0000000..ee87b0d
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/fr/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Pietroni <pietroni@informatique.univ-paris-diderot.fr>
+ */
+$lang['connectfail']           = 'Impossible de se connecter à la base de données.';
+$lang['userexists']            = 'Désolé, un utilisateur avec cet identifiant existe déjà.';
+$lang['writefail']             = 'Impossible de modifier les données utilisateur. Veuillez en informer l\'administrateur du Wiki.';
diff --git a/wiki/lib/plugins/authpdo/lang/hr/lang.php b/wiki/lib/plugins/authpdo/lang/hr/lang.php
new file mode 100644
index 0000000..3acdbf4
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/hr/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Davor Turkalj <turki.bsc@gmail.com>
+ */
+$lang['connectfail']           = 'Ne mogu se spojiti na bazu.';
+$lang['userexists']            = 'Oprostite ali korisnik s ovom prijavom već postoji.';
+$lang['writefail']             = 'Ne mogu izmijeniti podatke. Molim obavijestite Wiki administratora';
diff --git a/wiki/lib/plugins/authpdo/lang/hu/lang.php b/wiki/lib/plugins/authpdo/lang/hu/lang.php
new file mode 100644
index 0000000..1a2098e
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/hu/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Marton Sebok <sebokmarton@gmail.com>
+ */
+$lang['connectfail']           = 'Az adatbázishoz való csatlakozás sikertelen.';
+$lang['userexists']            = 'Sajnos már létezik ilyen azonosítójú felhasználó.';
+$lang['writefail']             = 'A felhasználói adatok módosítása sikertelen. Kérlek, fordulj a wiki rendszergazdájához!';
diff --git a/wiki/lib/plugins/authpdo/lang/it/lang.php b/wiki/lib/plugins/authpdo/lang/it/lang.php
new file mode 100644
index 0000000..5c0a3f1
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/it/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Torpedo <dgtorpedo@gmail.com>
+ */
+$lang['connectfail']           = 'Connessione fallita al database.';
+$lang['userexists']            = 'Spiacente, esiste già un utente con queste credenziali.';
+$lang['writefail']             = 'Non è possibile cambiare le informazioni utente. Si prega di informare l\'Amministratore del wiki';
diff --git a/wiki/lib/plugins/authpdo/lang/ja/lang.php b/wiki/lib/plugins/authpdo/lang/ja/lang.php
new file mode 100644
index 0000000..1cd441b
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/ja/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['connectfail']           = 'データベースへの接続に失敗しました。';
+$lang['userexists']            = 'このログイン名のユーザーが既に存在しています。';
+$lang['writefail']             = 'ユーザーデータを変更できません。Wiki の管理者に連絡してください。';
diff --git a/wiki/lib/plugins/authpdo/lang/ko/lang.php b/wiki/lib/plugins/authpdo/lang/ko/lang.php
new file mode 100644
index 0000000..0b14197
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/ko/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author hyeonsoft <hyeonsoft@live.co.kr>
+ * @author Myeongjin <aranet100@gmail.com>
+ */
+$lang['connectfail']           = '데이터베이스에 연결하는 데 실패했습니다.';
+$lang['userexists']            = '죄송하지만 이 계정으로 이미 로그인한 사용자가 있습니다.';
+$lang['writefail']             = '사용자 데이터를 수정할 수 없습니다. 위키 관리자에게 문의하시기 바랍니다';
diff --git a/wiki/lib/plugins/authpdo/lang/nl/lang.php b/wiki/lib/plugins/authpdo/lang/nl/lang.php
new file mode 100644
index 0000000..b426f6b
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/nl/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Hugo Smet <hugo.smet@scarlet.be>
+ */
+$lang['connectfail']           = 'Connectie met de database mislukt.';
+$lang['userexists']            = 'Sorry, een gebruiker met deze login bestaat reeds.';
+$lang['writefail']             = 'Onmogelijk om de gebruikers data te wijzigen. Gelieve de Wiki-Admin te informeren.';
diff --git a/wiki/lib/plugins/authpdo/lang/pt-br/lang.php b/wiki/lib/plugins/authpdo/lang/pt-br/lang.php
new file mode 100644
index 0000000..2008ae6
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/pt-br/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Frederico Gonçalves Guimarães <frederico@teia.bio.br>
+ */
+$lang['connectfail']           = 'Não foi possível conectar ao banco de dados.';
+$lang['userexists']            = 'Desculpe, mas já existe esse nome de usuário.';
+$lang['writefail']             = 'Não foi possível modificar os dados do usuário. Por favor, informe ao administrador do Wiki.';
diff --git a/wiki/lib/plugins/authpdo/lang/pt/lang.php b/wiki/lib/plugins/authpdo/lang/pt/lang.php
new file mode 100644
index 0000000..f2eca8f
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/pt/lang.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Paulo Carmino <contato@paulocarmino.com>
+ */
+$lang['connectfail']           = 'Falha ao conectar com o banco de dados.';
+$lang['userexists']            = 'Desculpe, esse login já está sendo usado.';
diff --git a/wiki/lib/plugins/authpdo/lang/ru/lang.php b/wiki/lib/plugins/authpdo/lang/ru/lang.php
new file mode 100644
index 0000000..9f75d17
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/ru/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Takumo <9206984@mail.ru>
+ * @author Aleksandr Selivanov <alexgearbox@yandex.ru>
+ */
+$lang['connectfail']           = 'Ошибка соединения с базой данных.';
+$lang['userexists']            = 'Извините, пользователь с таким логином уже существует.';
+$lang['writefail']             = 'Невозможно изменить данные пользователя. Сообщите об этом администратору вики.';
diff --git a/wiki/lib/plugins/authpdo/lang/sk/lang.php b/wiki/lib/plugins/authpdo/lang/sk/lang.php
new file mode 100644
index 0000000..d143bbf
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/sk/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Martin Michalek <michalek.dev@gmail.com>
+ */
+$lang['connectfail']           = 'Nepodarilo sa pripojiť k databáze.';
+$lang['userexists']            = 'Ľutujem, ale používateľ s týmto prihlasovacím menom už existuje.';
+$lang['writefail']             = 'Nie je možné zmeniť údaje používateľa, informujte prosím administrátora Wiki.';
diff --git a/wiki/lib/plugins/authpdo/lang/tr/lang.php b/wiki/lib/plugins/authpdo/lang/tr/lang.php
new file mode 100644
index 0000000..30576c0
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/tr/lang.php
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Mete Cuma <mcumax@gmail.com>
+ */
+$lang['connectfail']           = 'Veritabanına bağlantı kurulamadı.';
diff --git a/wiki/lib/plugins/authpdo/lang/zh/lang.php b/wiki/lib/plugins/authpdo/lang/zh/lang.php
new file mode 100644
index 0000000..06c258f
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/lang/zh/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * 
+ * @author Errol <errol@hotmail.com>
+ */
+$lang['connectfail']           = '连接数据库失败';
+$lang['userexists']            = '抱歉,用户名已被使用。';
+$lang['writefail']             = '无法修改用户数据。请通知管理员';
diff --git a/wiki/lib/plugins/authpdo/plugin.info.txt b/wiki/lib/plugins/authpdo/plugin.info.txt
new file mode 100644
index 0000000..e60ff0b
--- /dev/null
+++ b/wiki/lib/plugins/authpdo/plugin.info.txt
@@ -0,0 +1,7 @@
+base   authpdo
+author Andreas Gohr
+email  andi@splitbrain.org
+date   2016-08-20
+name   authpdo plugin
+desc   Authenticate against a database via PDO
+url    https://www.dokuwiki.org/plugin:authpdo