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