xref: /plugin/authwordpress/auth.php (revision 5d099ac18de58585b07e47ad428d8cffcf6a3582)
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     * @var string $sql_wp_user_data
48     */
49    protected $sql_wp_user_data = "SELECT
50            id, user_login, user_pass, user_email, display_name,
51            meta_value AS groups
52        FROM %prefix%users u
53        JOIN %prefix%usermeta m ON u.id = m.user_id AND meta_key = '%prefix%capabilities'";
54
55    /**
56     * Wordpress database connection
57     * @var PDO $db
58     */
59    protected $db;
60
61    /**
62     * Users cache
63     * @var array $users
64     */
65    protected $users;
66
67    /**
68     * True if all users have been loaded in the cache
69     * @see $users
70     * @var bool $usersCached
71     */
72    protected $usersCached = false;
73
74    /**
75     * Filter pattern
76     * @var array $filter
77     */
78    protected $filter;
79
80    /**
81     * Constructor.
82     */
83    public function __construct()
84    {
85        parent::__construct();
86
87        // Plugin capabilities
88        $this->cando['getUsers'] = true;
89        $this->cando['getUserCount'] = true;
90
91        // Try to establish a connection to the WordPress DB
92        // abort in case of failure
93        try {
94            $this->connectWordpressDb();
95        } catch (Exception $e) {
96            msg(sprintf($this->getLang('error_connect_failed'), $e->getMessage()));
97            $this->success = false;
98            return;
99        }
100
101        // Initialize SQL query with configured prefix
102        $this->sql_wp_user_data = str_replace(
103            '%prefix%',
104            $this->getConf('prefix'),
105            $this->sql_wp_user_data
106        );
107
108        $this->success = true;
109    }
110
111
112    /**
113     * Check user+password
114     *
115     * @param   string $user the user name
116     * @param   string $pass the clear text password
117     * @return  bool
118     *
119     * @uses PasswordHash::CheckPassword WordPress password hasher
120     */
121    public function checkPass($user, $pass)
122    {
123        $data = $this->getUserData($user);
124        if ($data === false) {
125            return false;
126        }
127
128        $hasher = new PasswordHash(8, true);
129        $check = $hasher->CheckPassword($pass, $data['pass']);
130        dbglog("Password " . ($check ? 'OK' : 'Invalid'));
131
132        return $check;
133    }
134
135    /**
136     * Bulk retrieval of user data
137     *
138     * @param   int   $start index of first user to be returned
139     * @param   int   $limit max number of users to be returned
140     * @param   array $filter array of field/pattern pairs
141     * @return  array userinfo (refer getUserData for internal userinfo details)
142     */
143    public function retrieveUsers($start = 0, $limit = 0, $filter = array())
144    {
145        msg($this->getLang('user_list_use_wordpress'));
146
147        $this->cacheAllUsers();
148
149        // Apply filter and pagination
150        $this->setFilter($filter);
151        $list = array();
152        foreach ($this->users as $user => $info) {
153            if ($this->applyFilter($user, $info)) {
154                if ($i >= $start) {
155                    $list[$user] = $info;
156                    $count++;
157                    if ($limit > 0 && $count >= $limit) {
158                        break;
159                    }
160                }
161                $i++;
162            }
163        }
164
165        return $list;
166    }
167
168    /**
169     * Return a count of the number of user which meet $filter criteria
170     *
171     * @param array $filter
172     * @return int
173     */
174    public function getUserCount($filter = array())
175    {
176        $this->cacheAllUsers();
177
178        if (empty($filter)) {
179            $count = count($this->users);
180        } else {
181            $this->setFilter($filter);
182            foreach ($this->users as $user => $info) {
183                $count += (int)$this->applyFilter($user, $info);
184            }
185        }
186        return $count;
187    }
188
189
190    /**
191     * Returns info about the given user
192     *
193     * @param string $user the user name
194     * @param bool   $requireGroups defaults to true
195     * @return array|false containing user data or false in case of error
196     */
197    public function getUserData($user, $requireGroups = true)
198    {
199        if (isset($this->users[$user])) {
200            return $this->users[$user];
201        }
202
203        $sql = $this->sql_wp_user_data
204            . 'WHERE user_login = :user';
205
206        $stmt = $this->db->prepare($sql);
207        $stmt->bindParam(':user', $user);
208        dbglog("Retrieving data for user '$user'\n$sql");
209
210        if (!$stmt->execute()) {
211            // Query execution failed
212            $err = $stmt->errorInfo();
213            dbglog("Error $err[1]: $err[2]");
214            return false;
215        }
216
217        $user = $stmt->fetch(PDO::FETCH_ASSOC);
218        if ($user === false) {
219            // Unknown user
220            dbglog("Unknown user");
221            return false;
222        }
223
224        return $this->cacheUser($user);
225    }
226
227
228    /**
229     * Connect to Wordpress database
230     * Initializes $db property as PDO object
231     */
232    protected function connectWordpressDb()
233    {
234        if ($this->db) {
235            // Already connected
236            return;
237        }
238
239        // Build connection string
240        $dsn = array(
241            'host=' . $this->getConf('hostname'),
242            'dbname=' . $this->getConf('database'),
243            'charset=UTF8',
244        );
245        $port = $this->getConf('port');
246        if ($port) {
247            $dsn[] = 'port=' . $port;
248        }
249        $dsn = 'mysql:' . implode(';', $dsn);
250
251        $this->db = new PDO($dsn, $this->getConf('username'), $this->getConf('password'));
252    }
253
254    /**
255     * Convert a Wordpress DB User row to DokuWiki user info array
256     * and stores it in the users cache
257     *
258     * @param  array $row Raw Wordpress user table row
259     * @return array user data
260     */
261    protected function cacheUser($row)
262    {
263        global $conf;
264
265        $login = $row['user_login'];
266
267        // If the user is already cached, just return it
268        if (isset($this->users[$login])) {
269            return $this->users[$login];
270        }
271
272        // Group membership - add DokuWiki's default group
273        $groups = array_keys(unserialize($row['groups']));
274        if ($this->getConf('usedefaultgroup')) {
275            $groups[] = $conf['defaultgroup'];
276        }
277
278        $info = array(
279            'user' => $login,
280            'name' => $row['display_name'],
281            'pass' => $row['user_pass'],
282            'mail' => $row['user_email'],
283            'grps' => $groups,
284        );
285
286        $this->users[$login] = $info;
287        return $info;
288    }
289
290    /**
291     * Loads all Wordpress users into the cache
292     *
293     * @return void
294     */
295    protected function cacheAllUsers()
296    {
297        if ($this->usersCached) {
298            return;
299        }
300
301        $stmt = $this->db->prepare($this->sql_wp_user_data);
302        $stmt->execute();
303
304        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $user) {
305            $this->cacheUser($user);
306        }
307
308        $this->usersCached = true;
309    }
310
311    /**
312     * Build filter patterns from given criteria
313     *
314     * @param array $filter
315     */
316    protected function setFilter($filter)
317    {
318        $this->filter = array();
319        foreach ($filter as $field => $value) {
320            // Build PCRE pattern, utf8 + case insensitive
321            $this->filter[$field] = '/' . str_replace('/', '\/', $value) . '/ui';
322        }
323    }
324
325    /**
326     * Return true if given user matches filter pattern, false otherwise
327     *
328     * @param string $user login
329     * @param array  $info User data
330     * @return bool
331     */
332    protected function applyFilter($user, $info)
333    {
334        foreach ($this->filter as $elem => $pattern) {
335            if ($elem == 'grps') {
336                if (!preg_grep($pattern, $info['grps'])) {
337                    return false;
338                }
339            } else {
340                if (!preg_match($pattern, $info[$elem])) {
341                    return false;
342                }
343            }
344        }
345        return true;
346    }
347}
348
349// vim:ts=4:sw=4:noet:
350