From f64dbc90055403db700941e4691ea451bb971cef Mon Sep 17 00:00:00 2001
From: Andreas Gohr <andi@splitbrain.org>
Date: Fri, 29 Jan 2016 21:25:50 +0100
Subject: [PATCH] began work on PDO auth plugin

---
 .gitignore                                 |   1 +
 lib/plugins/authpdo/.travis.yml            |  13 +
 lib/plugins/authpdo/README                 |  27 ++
 lib/plugins/authpdo/_test/general.test.php |  33 ++
 lib/plugins/authpdo/_test/sqlite.test.php  |  49 +++
 lib/plugins/authpdo/_test/test.sqlite3     | Bin 0 -> 14336 bytes
 lib/plugins/authpdo/auth.php               | 374 +++++++++++++++++++++
 lib/plugins/authpdo/conf/default.php       |  21 ++
 lib/plugins/authpdo/conf/metadata.php      |  10 +
 lib/plugins/authpdo/lang/en/lang.php       |  16 +
 lib/plugins/authpdo/lang/en/settings.php   |  13 +
 lib/plugins/authpdo/plugin.info.txt        |   7 +
 12 files changed, 564 insertions(+)
 create mode 100644 lib/plugins/authpdo/.travis.yml
 create mode 100644 lib/plugins/authpdo/README
 create mode 100644 lib/plugins/authpdo/_test/general.test.php
 create mode 100644 lib/plugins/authpdo/_test/sqlite.test.php
 create mode 100644 lib/plugins/authpdo/_test/test.sqlite3
 create mode 100644 lib/plugins/authpdo/auth.php
 create mode 100644 lib/plugins/authpdo/conf/default.php
 create mode 100644 lib/plugins/authpdo/conf/metadata.php
 create mode 100644 lib/plugins/authpdo/lang/en/lang.php
 create mode 100644 lib/plugins/authpdo/lang/en/settings.php
 create mode 100644 lib/plugins/authpdo/plugin.info.txt

diff --git a/.gitignore b/.gitignore
index 7410ee1c3..14cc1f129 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,7 @@
 !/lib/plugins/authldap
 !/lib/plugins/authmysql
 !/lib/plugins/authpgsql
+!/lib/plugins/authpdo
 !/lib/plugins/authplain
 !/lib/plugins/config
 !/lib/plugins/extension
diff --git a/lib/plugins/authpdo/.travis.yml b/lib/plugins/authpdo/.travis.yml
new file mode 100644
index 000000000..80368cad3
--- /dev/null
+++ b/lib/plugins/authpdo/.travis.yml
@@ -0,0 +1,13 @@
+# Config file for travis-ci.org
+
+language: php
+php:
+  - "5.5"
+  - "5.4"
+  - "5.3"
+env:
+  - DOKUWIKI=master
+  - DOKUWIKI=stable
+before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh
+install: sh travis.sh
+script: cd _test && phpunit --stderr --group plugin_authpdo
\ No newline at end of file
diff --git a/lib/plugins/authpdo/README b/lib/plugins/authpdo/README
new file mode 100644
index 000000000..c99bfbf81
--- /dev/null
+++ b/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/lib/plugins/authpdo/_test/general.test.php b/lib/plugins/authpdo/_test/general.test.php
new file mode 100644
index 000000000..6c48b6957
--- /dev/null
+++ b/lib/plugins/authpdo/_test/general.test.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * General tests for the authpdo plugin
+ *
+ * @group plugin_authpdo
+ * @group plugins
+ */
+class general_plugin_authpdo_test extends DokuWikiTest {
+
+    /**
+     * Simple test to make sure the plugin.info.txt is in correct format
+     */
+    public function test_plugininfo() {
+        $file = __DIR__.'/../plugin.info.txt';
+        $this->assertFileExists($file);
+
+        $info = confToHash($file);
+
+        $this->assertArrayHasKey('base', $info);
+        $this->assertArrayHasKey('author', $info);
+        $this->assertArrayHasKey('email', $info);
+        $this->assertArrayHasKey('date', $info);
+        $this->assertArrayHasKey('name', $info);
+        $this->assertArrayHasKey('desc', $info);
+        $this->assertArrayHasKey('url', $info);
+
+        $this->assertEquals('authpdo', $info['base']);
+        $this->assertRegExp('/^https?:\/\//', $info['url']);
+        $this->assertTrue(mail_isvalid($info['email']));
+        $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
+        $this->assertTrue(false !== strtotime($info['date']));
+    }
+}
diff --git a/lib/plugins/authpdo/_test/sqlite.test.php b/lib/plugins/authpdo/_test/sqlite.test.php
new file mode 100644
index 000000000..b60072d94
--- /dev/null
+++ b/lib/plugins/authpdo/_test/sqlite.test.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * General tests for the authpdo plugin
+ *
+ * @group plugin_authpdo
+ * @group plugins
+ */
+class sqlite_plugin_authpdo_test extends DokuWikiTest {
+
+    protected $dbfile;
+
+    public function setUp() {
+        parent::setUp();
+        $this->dbfile = tempnam('/tmp/', 'pluginpdo_test_');
+        copy(__DIR__ . '/test.sqlite3', $this->dbfile);
+
+        global $conf;
+
+        $conf['plugin']['authpdo']['debug'] = 1;
+        $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';
+    }
+
+    public function tearDown() {
+        parent::tearDown();
+        unlink($this->dbfile);
+    }
+
+    public function test_userinfo() {
+        global $conf;
+        $auth = new auth_plugin_authpdo();
+
+        // clear text pasword (with default config above
+        $this->assertFalse($auth->checkPass('nobody', 'nope'));
+        $this->assertFalse($auth->checkPass('admin', 'nope'));
+        $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';
+        $this->assertFalse($auth->checkPass('admin', 'password'));
+        $this->assertFalse($auth->checkPass('user', md5('password')));
+
+    }
+}
diff --git a/lib/plugins/authpdo/_test/test.sqlite3 b/lib/plugins/authpdo/_test/test.sqlite3
new file mode 100644
index 0000000000000000000000000000000000000000..403bf5f7224ab809f443f1af2b4ffc5e0ed9aa85
GIT binary patch
literal 14336
zcmeI3PjAym6u{?CYR6q#${`v>h}}^yZPXN6N@=UATC%KLRhy<XB!`|NH+G9foRlUJ
zT-y^zz6s(3?0yHn0usAgX@Lt+q+WL3gp@cWY*rwZT5qB^6VD%e-p`)#c;?OBt*qbh
z<ZkUCa2j%s#0aJ2mXw5$5hbHajE^#=!kCh_qwPOm2uH~DpVkO}MogL{yu|)yf3Pw7
z7hR=E74h^7C3`1LTr|XTf)c;#dXMT4Dyr@!r`f3K<5IKk9h7Fl|6zj}!ddQ@u~o9(
zXu5u_)bN7+Qp4G)c%|7-2~(F0@pH1@2?8(JforwG_~DOZrbs5~k<jO9#kTy>+j%=v
zuw^0hOV*aHI62w$UAdks*lTuP=IvEGZ|82?cO_I^PWo<2<~9p5SIlP9a<6yR@x6mu
zbAP-$_nrLuMkc>4f3vssRdo4OYG7`MQ=_IxB<RtS)&wd9VurM6SMyYb&Z}mMh0XPx
z%D-Xf3TauX?fF$%%&l(~?GCN>oqE0Fu<8Wf>7o49Ud<G<1vxn}kxI+J@hiR4QYp(2
zE27^`(u7L0Iv8%mO(6t5T+@2#IPF;>%{r+5w{oYIO3QY+E<ICsxFM#d=(fJ-$LF=~
zJ!pE>ve$JO3ss~maS{NfRmqfU7n@)8V27S~4vjq2s(ZIPj70)SAc6#jL8s2QdIWVu
zm5=}u=uKdRzaoskAp8Y?-#Z7!A^{|DehFBV3#vbLm{P*B;NvaMc!I(ohU5S9duOrc
zNZ=b07*gi}xu68P1g-?S3v@^T2}GCxG<L=x6aHLz=#T&sI1>VE%pi%4iD?*=&+p!F
z%jLP9dB<6}Iq%+_br<KZFSy>$;^Ou4?$T05R$;lWqL+)Hz%B2Q6YN*KtL0iSO${Qh
z{B+H61HTHZEDvi3ZsDFMGZ3L;JF~<5ALsuNe!}0X0J;NM^Meq^0_czc5{L)^Ge#+8
zj2kfv!dT3*tk(Pg72(HfA`l%CKmySvpr-wZ&_e)|a*}|d1HAtke@*yn{vo=Gqh?3|
z38>kBwG^P+KQnnkuLPX7|6|n&phE&kAi@OV?Pb3>7nbe_VEi8k+uUbi>)Z%K7)&*^
z{{@R^7$Sn8{cp>pI%M2@CicHyZP?yr|4V;Y=6`PpTVSK)4SfQ?!5YZ9%=O(H>i@59
zhrJ|Q<&}Qs@1OQEgl_pv=PrkEiV@Lm0))`Z1MTG?V}>}qf58+hEA+_kFxvlYtH?}0
zx<er|{_%d<)W(fIdqDaX`E0AlH`y2h?|%;8|0nz%egT}zJ`F341O_B9#JQgNXWXKU
IQ*K%R0$46pk^lez

literal 0
HcmV?d00001

diff --git a/lib/plugins/authpdo/auth.php b/lib/plugins/authpdo/auth.php
new file mode 100644
index 000000000..1325bdcff
--- /dev/null
+++ b/lib/plugins/authpdo/auth.php
@@ -0,0 +1,374 @@
+<?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 extends DokuWiki_Auth_Plugin {
+
+    /** @var PDO */
+    protected $pdo;
+
+    /**
+     * 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'),
+                $this->getConf('pass'),
+                array(
+                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
+                )
+            );
+        } catch(PDOException $e) {
+            $this->_debug($e);
+            $this->success = false;
+            return;
+        }
+
+        // FIXME set capabilities accordingly
+        //$this->cando['addUser']     = false; // can Users be created?
+        //$this->cando['delUser']     = false; // can Users be deleted?
+        //$this->cando['modLogin']    = false; // can login names be changed?
+        //$this->cando['modPass']     = false; // can passwords be changed?
+        //$this->cando['modName']     = false; // can real names be changed?
+        //$this->cando['modMail']     = false; // can emails be changed?
+        //$this->cando['modGroups']   = false; // can groups be changed?
+        //$this->cando['getUsers']    = false; // can a (filtered) list of users be retrieved?
+        //$this->cando['getUserCount']= false; // can the number of users be retrieved?
+        //$this->cando['getGroups']   = false; // can a list of available groups be retrieved?
+        //$this->cando['external']    = false; // does the module do external auth checking?
+        //$this->cando['logout']      = true; // can the user logout again? (eg. not possible with HTTP auth)
+
+        // FIXME intialize your auth system and set success to true, if successful
+        $this->success = true;
+    }
+
+    /**
+     * Check user+password
+     *
+     * May be ommited if trustExternal is used.
+     *
+     * @param   string $user the user name
+     * @param   string $pass the clear text password
+     * @return  bool
+     */
+    public function checkPass($user, $pass) {
+
+        $data = $this->_selectUser($user);
+        if($data == false) return false;
+
+        if(isset($data['hash'])) {
+            // hashed password
+            $passhash = new PassHash();
+            return $passhash->verify_hash($pass, $data['hash']);
+        } else {
+            // clear text password in the database O_o
+            return ($pass == $data['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 containing user data or false
+     */
+    public function getUserData($user, $requireGroups = true) {
+        $data = $this->_selectUser($user);
+        if($data == false) return false;
+
+        if($requireGroups) {
+
+        }
+
+        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 $pass
+     * @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;
+    //}
+
+    /**
+     * Modify user data [implement only where required/possible]
+     *
+     * Set the mod* capabilities according to the implemented features
+     *
+     * @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) {
+    // FIXME implement
+    //    return false;
+    //}
+
+    /**
+     * Delete one or more users [implement only where required/possible]
+     *
+     * Set delUser capability when implemented
+     *
+     * @param   array $users
+     * @return  int    number of users deleted
+     */
+    //public function deleteUsers($users) {
+    // FIXME implement
+    //    return false;
+    //}
+
+    /**
+     * 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) {
+    // FIXME implement
+    //    return array();
+    //}
+
+    /**
+     * Return a count of the number of user which meet $filter criteria
+     * [should be implemented whenever retrieveUsers is implemented]
+     *
+     * Set getUserCount capability when implemented
+     *
+     * @param  array $filter array of field/pattern pairs, empty array for no filter
+     * @return int
+     */
+    //public function getUserCount($filter = array()) {
+    // FIXME implement
+    //    return 0;
+    //}
+
+    /**
+     * Define a group [implement only where required/possible]
+     *
+     * Set addGroup capability when implemented
+     *
+     * @param   string $group
+     * @return  bool
+     */
+    //public function addGroup($group) {
+    // FIXME implement
+    //    return false;
+    //}
+
+    /**
+     * Retrieve groups [implement only where required/possible]
+     *
+     * Set getGroups capability when implemented
+     *
+     * @param   int $start
+     * @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;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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
+     */
+    protected function _selectUser($user) {
+        $sql = $this->getConf('select-user');
+
+        try {
+            $sth = $this->pdo->prepare($sql);
+            $sth->execute(array(':user' => $user));
+            $result = $sth->fetchAll();
+            $sth->closeCursor();
+            $sth = null;
+        } catch(PDOException $e) {
+            $this->_debug($e);
+            $result = array();
+        }
+        $found = count($result);
+        if($found == 0) return false;
+
+        if($found > 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->_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;
+    }
+
+    /**
+     * 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;
+            $line = $message->getLine();
+            $msg = $message->getMessage();
+        } else {
+            $msg = $message;
+        }
+
+        if(defined('DOKU_UNITTEST')) {
+            printf("\n%s, %s:%d\n", $msg, __FILE__, $line);
+        } else {
+            msg('authpdo: ' . $msg, $err, $line, __FILE__);
+        }
+    }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/lib/plugins/authpdo/conf/default.php b/lib/plugins/authpdo/conf/default.php
new file mode 100644
index 000000000..22f8369d0
--- /dev/null
+++ b/lib/plugins/authpdo/conf/default.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Default settings for the authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+//$conf['fixme']    = 'FIXME';
+
+$conf['debug'] = 0;
+$conf['dsn'] = '';
+$conf['user'] = '';
+$conf['pass'] = '';
+
+/**
+ * statement to select a single user identified by its login name given as :user
+ *
+ * return; user, name, mail, (clear|hash), [uid]
+ * other fields are returned but not used
+ */
+$conf['select-user'] = '';
diff --git a/lib/plugins/authpdo/conf/metadata.php b/lib/plugins/authpdo/conf/metadata.php
new file mode 100644
index 000000000..e020d5c45
--- /dev/null
+++ b/lib/plugins/authpdo/conf/metadata.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * Options for the authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+
+//$meta['fixme'] = array('string');
+
diff --git a/lib/plugins/authpdo/lang/en/lang.php b/lib/plugins/authpdo/lang/en/lang.php
new file mode 100644
index 000000000..de9f81252
--- /dev/null
+++ b/lib/plugins/authpdo/lang/en/lang.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * English language file for authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// menu entry for admin plugins
+// $lang['menu'] = 'Your menu entry';
+
+// custom language strings for the plugin
+// $lang['fixme'] = 'FIXME';
+
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/lib/plugins/authpdo/lang/en/settings.php b/lib/plugins/authpdo/lang/en/settings.php
new file mode 100644
index 000000000..503511ace
--- /dev/null
+++ b/lib/plugins/authpdo/lang/en/settings.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * english language file for authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// keys need to match the config setting name
+// $lang['fixme'] = 'FIXME';
+
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/lib/plugins/authpdo/plugin.info.txt b/lib/plugins/authpdo/plugin.info.txt
new file mode 100644
index 000000000..6784fd083
--- /dev/null
+++ b/lib/plugins/authpdo/plugin.info.txt
@@ -0,0 +1,7 @@
+base   authpdo
+author Andreas Gohr
+email  andi@splitbrain.org
+date   2016-01-29
+name   authpdo plugin
+desc   Authenticate against a database via PDO
+url    https://www.dokuwiki.org/plugin:authpdo
-- 
GitLab