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