1<?php
2/**
3 * DokuWiki Plugin authwordpress (Auth Component)
4 *
5 * Provides authentication against a WordPress MySQL database backend
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; version 2 of the License
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * See the COPYING file in your DokuWiki folder for details
17 *
18 * @author     Damien Regad <dregad@mantisbt.org>
19 * @copyright  2015 Damien Regad
20 * @license    GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
21 * @version    1.1
22 * @link       https://github.com/dregad/dokuwiki-authwordpress
23 */
24
25
26// must be run within Dokuwiki
27if (!defined('DOKU_INC')) {
28    die();
29}
30
31/**
32 * WordPress password hashing framework
33 */
34require_once('class-phpass.php');
35
36/**
37 * Authentication class
38 */
39// @codingStandardsIgnoreLine
40class auth_plugin_authwordpress extends DokuWiki_Auth_Plugin
41{
42
43    /**
44     * SQL statement to retrieve User data from WordPress DB
45     * (including group memberships)
46     * '%prefix%' will be replaced by the actual prefix (from plugin config)
47     */
48    protected $sql_wp_user_data = "SELECT
49            id, user_login, user_pass, user_email, display_name,
50            meta_value AS groups
51        FROM %prefix%users u
52        JOIN %prefix%usermeta m ON u.id = m.user_id AND meta_key = '%prefix%capabilities'";
53
54    /**
55     * Wordpress database connection
56     */
57    protected $db;
58
59    /**
60     * Users cache
61     */
62    protected $users;
63
64    /**
65     * True if all users have been loaded in the cache
66     * @see $users
67     */
68    protected $usersCached = false;
69
70    /**
71     * Filter pattern
72     */
73    protected $filter;
74
75    /**
76     * Constructor.
77     */
78    public function __construct()
79    {
80        parent::__construct();
81
82        // Plugin capabilities
83        $this->cando['getUsers'] = true;
84        $this->cando['getUserCount'] = true;
85
86        // Try to establish a connection to the WordPress DB
87        // abort in case of failure
88        try {
89            $this->connectWordpressDb();
90        } catch (Exception $e) {
91            msg(sprintf($this->getLang('error_connect_failed'), $e->getMessage()));
92            $this->success = false;
93            return;
94        }
95
96        // Initialize SQL query with configured prefix
97        $this->sql_wp_user_data = str_replace(
98            '%prefix%',
99            $this->getConf('prefix'),
100            $this->sql_wp_user_data
101        );
102
103        $this->success = true;
104    }
105
106
107    /**
108     * Check user+password
109     *
110     * @param   string $user the user name
111     * @param   string $pass the clear text password
112     * @return  bool
113     *
114     * @uses PasswordHash::CheckPassword WordPress password hasher
115     */
116    public function checkPass($user, $pass)
117    {
118        $data = $this->getUserData($user);
119        if ($data === false) {
120            return false;
121        }
122
123        $hasher = new PasswordHash(8, true);
124        $check = $hasher->CheckPassword($pass, $data['pass']);
125        dbglog("Password " . ($check ? 'OK' : 'Invalid'));
126
127        return $check;
128    }
129
130    /**
131     * Bulk retrieval of user data
132     *
133     * @param   int   $start index of first user to be returned
134     * @param   int   $limit max number of users to be returned
135     * @param   array $filter array of field/pattern pairs
136     * @return  array userinfo (refer getUserData for internal userinfo details)
137     */
138    public function retrieveUsers($start = 0, $limit = 0, $filter = array())
139    {
140        msg($this->getLang('user_list_use_wordpress'));
141
142        $this->cacheAllUsers();
143
144        // Apply filter and pagination
145        $this->setFilter($filter);
146        $list = array();
147        foreach ($this->users as $user => $info) {
148            if ($this->applyFilter($user, $info)) {
149                if ($i >= $start) {
150                    $list[$user] = $info;
151                    $count++;
152                    if ($limit > 0 && $count >= $limit) {
153                        break;
154                    }
155                }
156                $i++;
157            }
158        }
159
160        return $list;
161    }
162
163    /**
164     * Return a count of the number of user which meet $filter criteria
165     *
166     * @param array $filter
167     * @return int
168     */
169    public function getUserCount($filter = array())
170    {
171        $this->cacheAllUsers();
172
173        if (empty($filter)) {
174            $count = count($this->users);
175        } else {
176            $this->setFilter($filter);
177            foreach ($this->users as $user => $info) {
178                $count += (int)$this->applyFilter($user, $info);
179            }
180        }
181        return $count;
182    }
183
184
185    /**
186     * Returns info about the given user
187     *
188     * @param   string $user the user name
189     * @return  array containing user data or false
190     */
191    public function getUserData($user, $requireGroups = true)
192    {
193        if (isset($this->users[$user])) {
194            return $this->users[$user];
195        }
196
197        $sql = $this->sql_wp_user_data
198            . 'WHERE user_login = :user';
199
200        $stmt = $this->db->prepare($sql);
201        $stmt->bindParam(':user', $user);
202        dbglog("Retrieving data for user '$user'\n$sql");
203
204        if (!$stmt->execute()) {
205            // Query execution failed
206            $err = $stmt->errorInfo();
207            dbglog("Error $err[1]: $err[2]");
208            return false;
209        }
210
211        $user = $stmt->fetch(PDO::FETCH_ASSOC);
212        if ($user === false) {
213            // Unknown user
214            dbglog("Unknown user");
215            return false;
216        }
217
218        return $this->cacheUser($user);
219    }
220
221
222    /**
223     * Connect to Wordpress database
224     * Initializes $db property as PDO object
225     */
226    protected function connectWordpressDb()
227    {
228        if ($this->db) {
229            // Already connected
230            return;
231        }
232
233        // Build connection string
234        $dsn = array(
235            'host=' . $this->getConf('hostname'),
236            'dbname=' . $this->getConf('database'),
237            'charset=UTF8',
238        );
239        $port = $this->getConf('port');
240        if ($port) {
241            $dsn[] = 'port=' . $port;
242        }
243        $dsn = 'mysql:' . implode(';', $dsn);
244
245        $this->db = new PDO($dsn, $this->getConf('username'), $this->getConf('password'));
246    }
247
248    /**
249     * Convert a Wordpress DB User row to DokuWiki user info array
250     * and stores it in the users cache
251     *
252     * @param  array $user Raw Wordpress user table row
253     * @return array user data
254     */
255    protected function cacheUser($row)
256    {
257        global $conf;
258
259        $login = $row['user_login'];
260
261        // If the user is already cached, just return it
262        if (isset($this->users[$login])) {
263            return $this->users[$login];
264        }
265
266        // Group membership - add DokuWiki's default group
267        $groups = array_keys(unserialize($row['groups']));
268        if ($this->getConf('usedefaultgroup')) {
269            $groups[] = $conf['defaultgroup'];
270        }
271
272        $info = array(
273            'user' => $login,
274            'name' => $row['display_name'],
275            'pass' => $row['user_pass'],
276            'mail' => $row['user_email'],
277            'grps' => $groups,
278        );
279
280        $this->users[$login] = $info;
281        return $info;
282    }
283
284    /**
285     * Loads all Wordpress users into the cache
286     *
287     * @return void
288     */
289    protected function cacheAllUsers()
290    {
291        if ($this->usersCached) {
292            return;
293        }
294
295        $stmt = $this->db->prepare($this->sql_wp_user_data);
296        $stmt->execute();
297
298        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $user) {
299            $this->cacheUser($user);
300        }
301
302        $this->usersCached = true;
303    }
304
305    /**
306     * Build filter patterns from given criteria
307     *
308     * @param array $filter
309     */
310    protected function setFilter($filter)
311    {
312        $this->filter = array();
313        foreach ($filter as $field => $value) {
314            // Build PCRE pattern, utf8 + case insensitive
315            $this->filter[$field] = '/' . str_replace('/', '\/', $value) . '/ui';
316        }
317    }
318
319    /**
320     * Return true if given user matches filter pattern, false otherwise
321     *
322     * @param string $user login
323     * @param array  $info User data
324     * @return bool
325     */
326    protected function applyFilter($user, $info)
327    {
328        foreach ($this->filter as $elem => $pattern) {
329            if ($elem == 'grps') {
330                if (!preg_grep($pattern, $info['grps'])) {
331                    return false;
332                }
333            } else {
334                if (!preg_match($pattern, $info[$elem])) {
335                    return false;
336                }
337            }
338        }
339        return true;
340    }
341}
342
343// vim:ts=4:sw=4:noet:
344