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