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