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