xref: /plugin/authwordpress/auth.php (revision 224e5cddf88e6b0f83d2a602f1f1103bec30552a)
124cd6f55SDamien Regad<?php
2e03e23f3SDamien Regad
324cd6f55SDamien Regad/**
424cd6f55SDamien Regad * DokuWiki Plugin authwordpress (Auth Component)
524cd6f55SDamien Regad *
635dd80b8SDamien Regad * Provides authentication against a WordPress MySQL database backend
735dd80b8SDamien Regad *
835dd80b8SDamien Regad * This program is free software; you can redistribute it and/or modify
935dd80b8SDamien Regad * it under the terms of the GNU General Public License as published by
1035dd80b8SDamien Regad * the Free Software Foundation; version 2 of the License
1135dd80b8SDamien Regad *
1235dd80b8SDamien Regad * This program is distributed in the hope that it will be useful,
1335dd80b8SDamien Regad * but WITHOUT ANY WARRANTY; without even the implied warranty of
1435dd80b8SDamien Regad * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1535dd80b8SDamien Regad * GNU General Public License for more details.
1635dd80b8SDamien Regad *
1735dd80b8SDamien Regad * See the COPYING file in your DokuWiki folder for details
1835dd80b8SDamien Regad *
1924cd6f55SDamien Regad * @author     Damien Regad <dregad@mantisbt.org>
2035dd80b8SDamien Regad * @copyright  2015 Damien Regad
2135dd80b8SDamien Regad * @license    GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
22b5b72c15SDamien Regad * @version    1.1
2335dd80b8SDamien Regad * @link       https://github.com/dregad/dokuwiki-authwordpress
24d7435096SDamien Regad *
25d7435096SDamien Regad * @noinspection PhpComposerExtensionStubsInspection
26*224e5cddSDamien Regad * @noinspection PhpUnused
27*224e5cddSDamien Regad * @noinspection PhpMissingReturnTypeInspection
2824cd6f55SDamien Regad */
2924cd6f55SDamien Regad
3024cd6f55SDamien Regad// must be run within Dokuwiki
31b741ff68SDamien Regadif (!defined('DOKU_INC')) {
32b741ff68SDamien Regad    die();
33b741ff68SDamien Regad}
3424cd6f55SDamien Regad
35d46733c5SDamien Regaduse dokuwiki\Extension\AuthPlugin;
36308b48d3SDamien Regaduse dokuwiki\Logger;
37308b48d3SDamien Regad
3835dd80b8SDamien Regad/**
3935dd80b8SDamien Regad * WordPress password hashing framework
4035dd80b8SDamien Regad */
4135dd80b8SDamien Regadrequire_once('class-phpass.php');
4235dd80b8SDamien Regad
4335dd80b8SDamien Regad/**
4435dd80b8SDamien Regad * Authentication class
4535dd80b8SDamien Regad */
46b741ff68SDamien Regad// @codingStandardsIgnoreLine
47d46733c5SDamien Regadclass auth_plugin_authwordpress extends AuthPlugin
48b741ff68SDamien Regad{
4935dd80b8SDamien Regad    /**
5035dd80b8SDamien Regad     * SQL statement to retrieve User data from WordPress DB
5135dd80b8SDamien Regad     * (including group memberships)
52b5b72c15SDamien Regad     * '%prefix%' will be replaced by the actual prefix (from plugin config)
535d099ac1SDamien Regad     * @var string $sql_wp_user_data
5435dd80b8SDamien Regad     */
550341717fSDamien Regad    protected $sql_wp_user_data = "SELECT
5635dd80b8SDamien Regad            id, user_login, user_pass, user_email, display_name,
57493dbdc6SFerdinand Thiessen            meta_value AS grps
58b5b72c15SDamien Regad        FROM %prefix%users u
592ef3e6a1SDamien Regad        JOIN %prefix%usermeta m ON u.id = m.user_id AND meta_key = '%prefix%capabilities'";
6024cd6f55SDamien Regad
6124cd6f55SDamien Regad    /**
62015f33b2SDamien Regad     * Wordpress database connection
635d099ac1SDamien Regad     * @var PDO $db
64015f33b2SDamien Regad     */
650341717fSDamien Regad    protected $db;
66015f33b2SDamien Regad
67d35aa3ecSDamien Regad    /**
68d35aa3ecSDamien Regad     * Users cache
695d099ac1SDamien Regad     * @var array $users
70d35aa3ecSDamien Regad     */
71d35aa3ecSDamien Regad    protected $users;
72d35aa3ecSDamien Regad
73d35aa3ecSDamien Regad    /**
74d35aa3ecSDamien Regad     * True if all users have been loaded in the cache
75d35aa3ecSDamien Regad     * @see $users
765d099ac1SDamien Regad     * @var bool $usersCached
77d35aa3ecSDamien Regad     */
78d35aa3ecSDamien Regad    protected $usersCached = false;
79d35aa3ecSDamien Regad
809f5692a3SDamien Regad    /**
819f5692a3SDamien Regad     * Filter pattern
825d099ac1SDamien Regad     * @var array $filter
839f5692a3SDamien Regad     */
849f5692a3SDamien Regad    protected $filter;
85015f33b2SDamien Regad
86015f33b2SDamien Regad    /**
8724cd6f55SDamien Regad     * Constructor.
8824cd6f55SDamien Regad     */
89b741ff68SDamien Regad    public function __construct()
90b741ff68SDamien Regad    {
9135dd80b8SDamien Regad        parent::__construct();
9224cd6f55SDamien Regad
93aa69c0cfSDamien Regad        // Plugin capabilities
9416ceb666SDamien Regad        $this->cando['getUsers'] = true;
95aa69c0cfSDamien Regad        $this->cando['getUserCount'] = true;
9616ceb666SDamien Regad
9735dd80b8SDamien Regad        // Try to establish a connection to the WordPress DB
9835dd80b8SDamien Regad        // abort in case of failure
9935dd80b8SDamien Regad        try {
100b741ff68SDamien Regad            $this->connectWordpressDb();
101f4aa129bSDamien Regad        } catch (PDOException $e) {
10235dd80b8SDamien Regad            msg(sprintf($this->getLang('error_connect_failed'), $e->getMessage()));
10335dd80b8SDamien Regad            $this->success = false;
10435dd80b8SDamien Regad            return;
10535dd80b8SDamien Regad        }
10624cd6f55SDamien Regad
107b5b72c15SDamien Regad        // Initialize SQL query with configured prefix
108b5b72c15SDamien Regad        $this->sql_wp_user_data = str_replace(
109b5b72c15SDamien Regad            '%prefix%',
110b5b72c15SDamien Regad            $this->getConf('prefix'),
111b5b72c15SDamien Regad            $this->sql_wp_user_data
112b5b72c15SDamien Regad        );
113b5b72c15SDamien Regad
11424cd6f55SDamien Regad        $this->success = true;
11524cd6f55SDamien Regad    }
11624cd6f55SDamien Regad
11724cd6f55SDamien Regad
11824cd6f55SDamien Regad    /**
119d7435096SDamien Regad     * Check user+password.
12024cd6f55SDamien Regad     *
121bdb9e9d4SDamien Regad     * Starting with WordPress 6.8, passwords are bcrypt-hashed with standard
122bdb9e9d4SDamien Regad     * php functions.
123bdb9e9d4SDamien Regad     * {@see https://make.wordpress.org/core/2025/02/17/wordpress-6-8-will-use-bcrypt-for-password-hashing/}
124bdb9e9d4SDamien Regad     *
125bdb9e9d4SDamien Regad     * Earlier versions of WordPress add slashes to the password before generating the hash
126666403eeSDamien Regad     * {@see https://developer.wordpress.org/reference/functions/wp_magic_quotes/},
127666403eeSDamien Regad     * so we need to do the same otherwise password containing `\`, `'` or `"` will
128666403eeSDamien Regad     * never match ({@see https://github.com/dregad/dokuwiki-plugin-authwordpress/issues/23)}.
129666403eeSDamien Regad     *
13024cd6f55SDamien Regad     * @param   string $user the username
13124cd6f55SDamien Regad     * @param   string $pass the clear text password
132d7435096SDamien Regad     *
13324cd6f55SDamien Regad     * @return  bool
13435dd80b8SDamien Regad     *
13535dd80b8SDamien Regad     * @uses PasswordHash::CheckPassword WordPress password hasher
13624cd6f55SDamien Regad     */
137b741ff68SDamien Regad    public function checkPass($user, $pass)
138b741ff68SDamien Regad    {
13935dd80b8SDamien Regad        $data = $this->getUserData($user);
14035dd80b8SDamien Regad        if ($data === false) {
14135dd80b8SDamien Regad            return false;
14224cd6f55SDamien Regad        }
143bdb9e9d4SDamien Regad        // Check for WordPress 6.8+ type hash
144bc637d09STim Oberländer        if (str_starts_with($data['pass'], '$wp')) {
145bc637d09STim Oberländer            $password_to_verify = base64_encode(hash_hmac('sha384', $pass, 'wp-sha384', true));
146bc637d09STim Oberländer            $check = password_verify($password_to_verify, substr($data['pass'], 3));
147bc637d09STim Oberländer        } else {
14835dd80b8SDamien Regad            $hasher = new PasswordHash(8, true);
149666403eeSDamien Regad            // Add slashes to match WordPress behavior
150666403eeSDamien Regad            $check = $hasher->CheckPassword(addslashes($pass), $data['pass']);
151bc637d09STim Oberländer        }
152308b48d3SDamien Regad        $this->logDebug("Password " . ($check ? 'OK' : 'Invalid'));
153eed09871SDamien Regad
154eed09871SDamien Regad        return $check;
15535dd80b8SDamien Regad    }
15635dd80b8SDamien Regad
15716ceb666SDamien Regad    /**
158d7435096SDamien Regad     * Bulk retrieval of user data.
15916ceb666SDamien Regad     *
16016ceb666SDamien Regad     * @param   int   $start index of first user to be returned
16116ceb666SDamien Regad     * @param   int   $limit max number of users to be returned
16216ceb666SDamien Regad     * @param   array $filter array of field/pattern pairs
163d7435096SDamien Regad     *
16416ceb666SDamien Regad     * @return  array userinfo (refer getUserData for internal userinfo details)
16516ceb666SDamien Regad     */
166b741ff68SDamien Regad    public function retrieveUsers($start = 0, $limit = 0, $filter = array())
167b741ff68SDamien Regad    {
16816ceb666SDamien Regad        msg($this->getLang('user_list_use_wordpress'));
1692ef3e6a1SDamien Regad
170d35aa3ecSDamien Regad        $this->cacheAllUsers();
1719f5692a3SDamien Regad
1729f5692a3SDamien Regad        // Apply filter and pagination
1739f5692a3SDamien Regad        $this->setFilter($filter);
1749f5692a3SDamien Regad        $list = array();
175acc20be0SDamien Regad        $count = $i = 0;
1769f5692a3SDamien Regad        foreach ($this->users as $user => $info) {
1779f5692a3SDamien Regad            if ($this->applyFilter($user, $info)) {
1789f5692a3SDamien Regad                if ($i >= $start) {
1799f5692a3SDamien Regad                    $list[$user] = $info;
1809f5692a3SDamien Regad                    $count++;
1819f5692a3SDamien Regad                    if ($limit > 0 && $count >= $limit) {
1829f5692a3SDamien Regad                        break;
1839f5692a3SDamien Regad                    }
1849f5692a3SDamien Regad                }
1859f5692a3SDamien Regad                $i++;
1869f5692a3SDamien Regad            }
1879f5692a3SDamien Regad        }
1889f5692a3SDamien Regad
1899f5692a3SDamien Regad        return $list;
19016ceb666SDamien Regad    }
19116ceb666SDamien Regad
192aa69c0cfSDamien Regad    /**
193d7435096SDamien Regad     * Return a count of the number of user which meet $filter criteria.
194aa69c0cfSDamien Regad     *
195aa69c0cfSDamien Regad     * @param array $filter
196d7435096SDamien Regad     *
197aa69c0cfSDamien Regad     * @return int
198aa69c0cfSDamien Regad     */
199b741ff68SDamien Regad    public function getUserCount($filter = array())
200b741ff68SDamien Regad    {
201aa69c0cfSDamien Regad        $this->cacheAllUsers();
2029f5692a3SDamien Regad
2039f5692a3SDamien Regad        if (empty($filter)) {
2049f5692a3SDamien Regad            $count = count($this->users);
2059f5692a3SDamien Regad        } else {
2069f5692a3SDamien Regad            $this->setFilter($filter);
207acc20be0SDamien Regad            $count = 0;
2089f5692a3SDamien Regad            foreach ($this->users as $user => $info) {
2099f5692a3SDamien Regad                $count += (int)$this->applyFilter($user, $info);
2109f5692a3SDamien Regad            }
2119f5692a3SDamien Regad        }
2129f5692a3SDamien Regad        return $count;
213aa69c0cfSDamien Regad    }
214aa69c0cfSDamien Regad
21535dd80b8SDamien Regad
21624cd6f55SDamien Regad    /**
217d7435096SDamien Regad     * Returns info about the given user.
21824cd6f55SDamien Regad     *
21924cd6f55SDamien Regad     * @param string $user the user name
2205d099ac1SDamien Regad     * @param bool   $requireGroups defaults to true
221d7435096SDamien Regad     *
2225d099ac1SDamien Regad     * @return array|false containing user data or false in case of error
22324cd6f55SDamien Regad     */
224b741ff68SDamien Regad    public function getUserData($user, $requireGroups = true)
225b741ff68SDamien Regad    {
226d35aa3ecSDamien Regad        if (isset($this->users[$user])) {
227d35aa3ecSDamien Regad            return $this->users[$user];
228d35aa3ecSDamien Regad        }
229d35aa3ecSDamien Regad
2302ef3e6a1SDamien Regad        $sql = $this->sql_wp_user_data
2312ef3e6a1SDamien Regad            . 'WHERE user_login = :user';
23235dd80b8SDamien Regad
2332ef3e6a1SDamien Regad        $stmt = $this->db->prepare($sql);
23435dd80b8SDamien Regad        $stmt->bindParam(':user', $user);
235308b48d3SDamien Regad        $this->logDebug("Retrieving data for user '$user'\n$sql");
23635dd80b8SDamien Regad
23735dd80b8SDamien Regad        if (!$stmt->execute()) {
2389520968dSDamien Regad            // Query execution failed
239eed09871SDamien Regad            $err = $stmt->errorInfo();
240308b48d3SDamien Regad            $this->logDebug("Error $err[1]: $err[2]");
24124cd6f55SDamien Regad            return false;
24224cd6f55SDamien Regad        }
2439520968dSDamien Regad
2449520968dSDamien Regad        $user = $stmt->fetch(PDO::FETCH_ASSOC);
2459520968dSDamien Regad        if ($user === false) {
2469520968dSDamien Regad            // Unknown user
247308b48d3SDamien Regad            $this->logDebug("Unknown user");
2489520968dSDamien Regad            return false;
2499520968dSDamien Regad        }
25024cd6f55SDamien Regad
251d35aa3ecSDamien Regad        return $this->cacheUser($user);
25224cd6f55SDamien Regad    }
25324cd6f55SDamien Regad
25424cd6f55SDamien Regad
25524cd6f55SDamien Regad    /**
256d7435096SDamien Regad     * Connect to Wordpress database.
257d7435096SDamien Regad     *
258d7435096SDamien Regad     * Initializes $db property as PDO object.
259d7435096SDamien Regad     *
260d7435096SDamien Regad     * @return void
261f4aa129bSDamien Regad     * @throws PDOException
26224cd6f55SDamien Regad     */
263d7435096SDamien Regad    protected function connectWordpressDb(): void
264b741ff68SDamien Regad    {
265cb81639bSDamien Regad        if ($this->db) {
266cb81639bSDamien Regad            // Already connected
267cb81639bSDamien Regad            return;
268cb81639bSDamien Regad        }
269cb81639bSDamien Regad
270cb81639bSDamien Regad        // Build connection string
27135dd80b8SDamien Regad        $dsn = array(
27235dd80b8SDamien Regad            'host=' . $this->getConf('hostname'),
27335dd80b8SDamien Regad            'dbname=' . $this->getConf('database'),
27402fbe6b4SDamien Regad            'charset=UTF8',
27535dd80b8SDamien Regad        );
27635dd80b8SDamien Regad        $port = $this->getConf('port');
27735dd80b8SDamien Regad        if ($port) {
27835dd80b8SDamien Regad            $dsn[] = 'port=' . $port;
27935dd80b8SDamien Regad        }
28035dd80b8SDamien Regad        $dsn = 'mysql:' . implode(';', $dsn);
28135dd80b8SDamien Regad
282015f33b2SDamien Regad        $this->db = new PDO($dsn, $this->getConf('username'), $this->getConf('password'));
28324cd6f55SDamien Regad    }
28424cd6f55SDamien Regad
285d1f83a80SDamien Regad    /**
286d7435096SDamien Regad     * Cache User Data.
287d7435096SDamien Regad     *
288d1f83a80SDamien Regad     * Convert a Wordpress DB User row to DokuWiki user info array
289d7435096SDamien Regad     * and stores it in the users cache.
290d1f83a80SDamien Regad     *
2915d099ac1SDamien Regad     * @param  array $row Raw Wordpress user table row
292d7435096SDamien Regad     *
293d1f83a80SDamien Regad     * @return array user data
294d1f83a80SDamien Regad     */
295d7435096SDamien Regad    protected function cacheUser(array $row): array
296b741ff68SDamien Regad    {
297d1f83a80SDamien Regad        global $conf;
298d1f83a80SDamien Regad
299d35aa3ecSDamien Regad        $login = $row['user_login'];
300d35aa3ecSDamien Regad
301d35aa3ecSDamien Regad        // If the user is already cached, just return it
302d35aa3ecSDamien Regad        if (isset($this->users[$login])) {
303d35aa3ecSDamien Regad            return $this->users[$login];
304d35aa3ecSDamien Regad        }
305d35aa3ecSDamien Regad
306d1f83a80SDamien Regad        // Group membership - add DokuWiki's default group
307493dbdc6SFerdinand Thiessen        $groups = array_keys(unserialize($row['grps']));
308d1f83a80SDamien Regad        if ($this->getConf('usedefaultgroup')) {
309d1f83a80SDamien Regad            $groups[] = $conf['defaultgroup'];
310d1f83a80SDamien Regad        }
311d1f83a80SDamien Regad
312d1f83a80SDamien Regad        $info = array(
313d35aa3ecSDamien Regad            'user' => $login,
314d35aa3ecSDamien Regad            'name' => $row['display_name'],
315d35aa3ecSDamien Regad            'pass' => $row['user_pass'],
316d35aa3ecSDamien Regad            'mail' => $row['user_email'],
317d1f83a80SDamien Regad            'grps' => $groups,
318d1f83a80SDamien Regad        );
319d35aa3ecSDamien Regad
320d35aa3ecSDamien Regad        $this->users[$login] = $info;
321d1f83a80SDamien Regad        return $info;
322d1f83a80SDamien Regad    }
323d1f83a80SDamien Regad
324d35aa3ecSDamien Regad    /**
325d7435096SDamien Regad     * Loads all Wordpress users into the cache.
326d35aa3ecSDamien Regad     *
327d35aa3ecSDamien Regad     * @return void
328d35aa3ecSDamien Regad     */
329b741ff68SDamien Regad    protected function cacheAllUsers()
330b741ff68SDamien Regad    {
331d35aa3ecSDamien Regad        if ($this->usersCached) {
332d35aa3ecSDamien Regad            return;
333d35aa3ecSDamien Regad        }
334d35aa3ecSDamien Regad
335d35aa3ecSDamien Regad        $stmt = $this->db->prepare($this->sql_wp_user_data);
336d35aa3ecSDamien Regad        $stmt->execute();
337d35aa3ecSDamien Regad
338d35aa3ecSDamien Regad        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $user) {
339d35aa3ecSDamien Regad            $this->cacheUser($user);
340d35aa3ecSDamien Regad        }
341d35aa3ecSDamien Regad
342d35aa3ecSDamien Regad        $this->usersCached = true;
343d35aa3ecSDamien Regad    }
344d35aa3ecSDamien Regad
3459f5692a3SDamien Regad    /**
346d7435096SDamien Regad     * Build filter patterns from given criteria.
3479f5692a3SDamien Regad     *
3489f5692a3SDamien Regad     * @param array $filter
349d7435096SDamien Regad     *
350d7435096SDamien Regad     * @return void
3519f5692a3SDamien Regad     */
352d7435096SDamien Regad    protected function setFilter(array $filter): void
353b741ff68SDamien Regad    {
3549f5692a3SDamien Regad        $this->filter = array();
3559f5692a3SDamien Regad        foreach ($filter as $field => $value) {
3569f5692a3SDamien Regad            // Build PCRE pattern, utf8 + case insensitive
3579f5692a3SDamien Regad            $this->filter[$field] = '/' . str_replace('/', '\/', $value) . '/ui';
3589f5692a3SDamien Regad        }
3599f5692a3SDamien Regad    }
3609f5692a3SDamien Regad
3619f5692a3SDamien Regad    /**
362d7435096SDamien Regad     * Return true if given user matches filter pattern, false otherwise.
3639f5692a3SDamien Regad     *
3649f5692a3SDamien Regad     * @param string $user login
3659f5692a3SDamien Regad     * @param array  $info User data
366d7435096SDamien Regad     *
3679f5692a3SDamien Regad     * @return bool
368*224e5cddSDamien Regad     * @noinspection PhpUnusedParameterInspection
3699f5692a3SDamien Regad     */
370d7435096SDamien Regad    protected function applyFilter(string $user, array $info): bool
371b741ff68SDamien Regad    {
3729f5692a3SDamien Regad        foreach ($this->filter as $elem => $pattern) {
3739f5692a3SDamien Regad            if ($elem == 'grps') {
37437a3480aSDamien Regad                if (!preg_grep($pattern, $info['grps'])) {
3759f5692a3SDamien Regad                    return false;
3769f5692a3SDamien Regad                }
3779f5692a3SDamien Regad            } else {
3789f5692a3SDamien Regad                if (!preg_match($pattern, $info[$elem])) {
3799f5692a3SDamien Regad                    return false;
3809f5692a3SDamien Regad                }
3819f5692a3SDamien Regad            }
3829f5692a3SDamien Regad        }
3839f5692a3SDamien Regad        return true;
3849f5692a3SDamien Regad    }
385308b48d3SDamien Regad
386308b48d3SDamien Regad    /**
387308b48d3SDamien Regad     * Add message to debug log.
388308b48d3SDamien Regad     *
389308b48d3SDamien Regad     * @param string $msg
390308b48d3SDamien Regad     *
391308b48d3SDamien Regad     * @return void
392308b48d3SDamien Regad     */
393308b48d3SDamien Regad    protected function logDebug(string $msg): void
394308b48d3SDamien Regad    {
395308b48d3SDamien Regad        global $updateVersion;
396308b48d3SDamien Regad        if ($updateVersion >= 52) {
397308b48d3SDamien Regad            Logger::debug($msg);
398308b48d3SDamien Regad        } else {
399*224e5cddSDamien Regad            /** @noinspection PhpDeprecationInspection */
400308b48d3SDamien Regad            dbglog($msg);
401308b48d3SDamien Regad        }
402308b48d3SDamien Regad    }
40324cd6f55SDamien Regad}
40424cd6f55SDamien Regad
4050e6cb03cSDamien Regad// vim:ts=4:sw=4:noet:
406