1<?php 2 3/** 4 * DokuWiki Plugin authwordpress (Auth Component) 5 * 6 * Provides authentication against a WordPress MySQL database backend 7 * 8 * This program is free software; you can redistribute it and/or modify 9 * it under the terms of the GNU General Public License as published by 10 * the Free Software Foundation; version 2 of the License 11 * 12 * This program is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * See the COPYING file in your DokuWiki folder for details 18 * 19 * @author Damien Regad <dregad@mantisbt.org> 20 * @copyright 2015 Damien Regad 21 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 22 * @version 1.1 23 * @link https://github.com/dregad/dokuwiki-authwordpress 24 * 25 * @noinspection PhpComposerExtensionStubsInspection 26 * PhpUnused 27 * PhpMissingReturnTypeInspection 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 * SQL statement to retrieve User data from WordPress DB 50 * (including group memberships) 51 * '%prefix%' will be replaced by the actual prefix (from plugin config) 52 * @var string $sql_wp_user_data 53 */ 54 protected $sql_wp_user_data = "SELECT 55 id, user_login, user_pass, user_email, display_name, 56 meta_value AS grps 57 FROM %prefix%users u 58 JOIN %prefix%usermeta m ON u.id = m.user_id AND meta_key = '%prefix%capabilities'"; 59 60 /** 61 * Wordpress database connection 62 * @var PDO $db 63 */ 64 protected $db; 65 66 /** 67 * Users cache 68 * @var array $users 69 */ 70 protected $users; 71 72 /** 73 * True if all users have been loaded in the cache 74 * @see $users 75 * @var bool $usersCached 76 */ 77 protected $usersCached = false; 78 79 /** 80 * Filter pattern 81 * @var array $filter 82 */ 83 protected $filter; 84 85 /** 86 * Constructor. 87 */ 88 public function __construct() 89 { 90 parent::__construct(); 91 92 // Plugin capabilities 93 $this->cando['getUsers'] = true; 94 $this->cando['getUserCount'] = true; 95 96 // Try to establish a connection to the WordPress DB 97 // abort in case of failure 98 try { 99 $this->connectWordpressDb(); 100 } catch (Exception $e) { 101 msg(sprintf($this->getLang('error_connect_failed'), $e->getMessage())); 102 $this->success = false; 103 return; 104 } 105 106 // Initialize SQL query with configured prefix 107 $this->sql_wp_user_data = str_replace( 108 '%prefix%', 109 $this->getConf('prefix'), 110 $this->sql_wp_user_data 111 ); 112 113 $this->success = true; 114 } 115 116 117 /** 118 * Check user+password. 119 * 120 * Starting with WordPress 6.8, passwords are bcrypt-hashed with standard 121 * php functions. 122 * {@see https://make.wordpress.org/core/2025/02/17/wordpress-6-8-will-use-bcrypt-for-password-hashing/} 123 * 124 * Earlier versions of WordPress add slashes to the password before generating the hash 125 * {@see https://developer.wordpress.org/reference/functions/wp_magic_quotes/}, 126 * so we need to do the same otherwise password containing `\`, `'` or `"` will 127 * never match ({@see https://github.com/dregad/dokuwiki-plugin-authwordpress/issues/23)}. 128 * 129 * @param string $user the username 130 * @param string $pass the clear text password 131 * 132 * @return bool 133 * 134 * @uses PasswordHash::CheckPassword WordPress password hasher 135 */ 136 public function checkPass($user, $pass) 137 { 138 $data = $this->getUserData($user); 139 if ($data === false) { 140 return false; 141 } 142 // Check for WordPress 6.8+ type hash 143 if (str_starts_with($data['pass'], '$wp')) { 144 $password_to_verify = base64_encode(hash_hmac('sha384', $pass, 'wp-sha384', true)); 145 $check = password_verify($password_to_verify, substr($data['pass'], 3)); 146 } else { 147 $hasher = new PasswordHash(8, true); 148 // Add slashes to match WordPress behavior 149 $check = $hasher->CheckPassword(addslashes($pass), $data['pass']); 150 } 151 $this->logDebug("Password " . ($check ? 'OK' : 'Invalid')); 152 153 return $check; 154 } 155 156 /** 157 * Bulk retrieval of user data. 158 * 159 * @param int $start index of first user to be returned 160 * @param int $limit max number of users to be returned 161 * @param array $filter array of field/pattern pairs 162 * 163 * @return array userinfo (refer getUserData for internal userinfo details) 164 */ 165 public function retrieveUsers($start = 0, $limit = 0, $filter = array()) 166 { 167 msg($this->getLang('user_list_use_wordpress')); 168 169 $this->cacheAllUsers(); 170 171 // Apply filter and pagination 172 $this->setFilter($filter); 173 $list = array(); 174 $count = $i = 0; 175 foreach ($this->users as $user => $info) { 176 if ($this->applyFilter($user, $info)) { 177 if ($i >= $start) { 178 $list[$user] = $info; 179 $count++; 180 if ($limit > 0 && $count >= $limit) { 181 break; 182 } 183 } 184 $i++; 185 } 186 } 187 188 return $list; 189 } 190 191 /** 192 * Return a count of the number of user which meet $filter criteria. 193 * 194 * @param array $filter 195 * 196 * @return int 197 */ 198 public function getUserCount($filter = array()) 199 { 200 $this->cacheAllUsers(); 201 202 if (empty($filter)) { 203 $count = count($this->users); 204 } else { 205 $this->setFilter($filter); 206 $count = 0; 207 foreach ($this->users as $user => $info) { 208 $count += (int)$this->applyFilter($user, $info); 209 } 210 } 211 return $count; 212 } 213 214 215 /** 216 * Returns info about the given user. 217 * 218 * @param string $user the user name 219 * @param bool $requireGroups defaults to true 220 * 221 * @return array|false containing user data or false in case of error 222 */ 223 public function getUserData($user, $requireGroups = true) 224 { 225 if (isset($this->users[$user])) { 226 return $this->users[$user]; 227 } 228 229 $sql = $this->sql_wp_user_data 230 . 'WHERE user_login = :user'; 231 232 $stmt = $this->db->prepare($sql); 233 $stmt->bindParam(':user', $user); 234 $this->logDebug("Retrieving data for user '$user'\n$sql"); 235 236 if (!$stmt->execute()) { 237 // Query execution failed 238 $err = $stmt->errorInfo(); 239 $this->logDebug("Error $err[1]: $err[2]"); 240 return false; 241 } 242 243 $user = $stmt->fetch(PDO::FETCH_ASSOC); 244 if ($user === false) { 245 // Unknown user 246 $this->logDebug("Unknown user"); 247 return false; 248 } 249 250 return $this->cacheUser($user); 251 } 252 253 254 /** 255 * Connect to Wordpress database. 256 * 257 * Initializes $db property as PDO object. 258 * 259 * @return void 260 */ 261 protected function connectWordpressDb(): void 262 { 263 if ($this->db) { 264 // Already connected 265 return; 266 } 267 268 // Build connection string 269 $dsn = array( 270 'host=' . $this->getConf('hostname'), 271 'dbname=' . $this->getConf('database'), 272 'charset=UTF8', 273 ); 274 $port = $this->getConf('port'); 275 if ($port) { 276 $dsn[] = 'port=' . $port; 277 } 278 $dsn = 'mysql:' . implode(';', $dsn); 279 280 $this->db = new PDO($dsn, $this->getConf('username'), $this->getConf('password')); 281 } 282 283 /** 284 * Cache User Data. 285 * 286 * Convert a Wordpress DB User row to DokuWiki user info array 287 * and stores it in the users cache. 288 * 289 * @param array $row Raw Wordpress user table row 290 * 291 * @return array user data 292 */ 293 protected function cacheUser(array $row): array 294 { 295 global $conf; 296 297 $login = $row['user_login']; 298 299 // If the user is already cached, just return it 300 if (isset($this->users[$login])) { 301 return $this->users[$login]; 302 } 303 304 // Group membership - add DokuWiki's default group 305 $groups = array_keys(unserialize($row['grps'])); 306 if ($this->getConf('usedefaultgroup')) { 307 $groups[] = $conf['defaultgroup']; 308 } 309 310 $info = array( 311 'user' => $login, 312 'name' => $row['display_name'], 313 'pass' => $row['user_pass'], 314 'mail' => $row['user_email'], 315 'grps' => $groups, 316 ); 317 318 $this->users[$login] = $info; 319 return $info; 320 } 321 322 /** 323 * Loads all Wordpress users into the cache. 324 * 325 * @return void 326 */ 327 protected function cacheAllUsers() 328 { 329 if ($this->usersCached) { 330 return; 331 } 332 333 $stmt = $this->db->prepare($this->sql_wp_user_data); 334 $stmt->execute(); 335 336 foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $user) { 337 $this->cacheUser($user); 338 } 339 340 $this->usersCached = true; 341 } 342 343 /** 344 * Build filter patterns from given criteria. 345 * 346 * @param array $filter 347 * 348 * @return void 349 */ 350 protected function setFilter(array $filter): void 351 { 352 $this->filter = array(); 353 foreach ($filter as $field => $value) { 354 // Build PCRE pattern, utf8 + case insensitive 355 $this->filter[$field] = '/' . str_replace('/', '\/', $value) . '/ui'; 356 } 357 } 358 359 /** 360 * Return true if given user matches filter pattern, false otherwise. 361 * 362 * @param string $user login 363 * @param array $info User data 364 * 365 * @return bool 366 */ 367 protected function applyFilter(string $user, array $info): bool 368 { 369 foreach ($this->filter as $elem => $pattern) { 370 if ($elem == 'grps') { 371 if (!preg_grep($pattern, $info['grps'])) { 372 return false; 373 } 374 } else { 375 if (!preg_match($pattern, $info[$elem])) { 376 return false; 377 } 378 } 379 } 380 return true; 381 } 382 383 /** 384 * Add message to debug log. 385 * 386 * @param string $msg 387 * 388 * @return void 389 */ 390 protected function logDebug(string $msg): void 391 { 392 global $updateVersion; 393 if ($updateVersion >= 52) { 394 Logger::debug($msg); 395 } else { 396 dbglog($msg); 397 } 398 } 399} 400 401// vim:ts=4:sw=4:noet: 402