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