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 grps 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 $count = $i = 0; 153 foreach ($this->users as $user => $info) { 154 if ($this->applyFilter($user, $info)) { 155 if ($i >= $start) { 156 $list[$user] = $info; 157 $count++; 158 if ($limit > 0 && $count >= $limit) { 159 break; 160 } 161 } 162 $i++; 163 } 164 } 165 166 return $list; 167 } 168 169 /** 170 * Return a count of the number of user which meet $filter criteria 171 * 172 * @param array $filter 173 * @return int 174 */ 175 public function getUserCount($filter = array()) 176 { 177 $this->cacheAllUsers(); 178 179 if (empty($filter)) { 180 $count = count($this->users); 181 } else { 182 $this->setFilter($filter); 183 $count = 0; 184 foreach ($this->users as $user => $info) { 185 $count += (int)$this->applyFilter($user, $info); 186 } 187 } 188 return $count; 189 } 190 191 192 /** 193 * Returns info about the given user 194 * 195 * @param string $user the user name 196 * @param bool $requireGroups defaults to true 197 * @return array|false containing user data or false in case of error 198 */ 199 public function getUserData($user, $requireGroups = true) 200 { 201 if (isset($this->users[$user])) { 202 return $this->users[$user]; 203 } 204 205 $sql = $this->sql_wp_user_data 206 . 'WHERE user_login = :user'; 207 208 $stmt = $this->db->prepare($sql); 209 $stmt->bindParam(':user', $user); 210 dbglog("Retrieving data for user '$user'\n$sql"); 211 212 if (!$stmt->execute()) { 213 // Query execution failed 214 $err = $stmt->errorInfo(); 215 dbglog("Error $err[1]: $err[2]"); 216 return false; 217 } 218 219 $user = $stmt->fetch(PDO::FETCH_ASSOC); 220 if ($user === false) { 221 // Unknown user 222 dbglog("Unknown user"); 223 return false; 224 } 225 226 return $this->cacheUser($user); 227 } 228 229 230 /** 231 * Connect to Wordpress database 232 * Initializes $db property as PDO object 233 */ 234 protected function connectWordpressDb() 235 { 236 if ($this->db) { 237 // Already connected 238 return; 239 } 240 241 // Build connection string 242 $dsn = array( 243 'host=' . $this->getConf('hostname'), 244 'dbname=' . $this->getConf('database'), 245 'charset=UTF8', 246 ); 247 $port = $this->getConf('port'); 248 if ($port) { 249 $dsn[] = 'port=' . $port; 250 } 251 $dsn = 'mysql:' . implode(';', $dsn); 252 253 $this->db = new PDO($dsn, $this->getConf('username'), $this->getConf('password')); 254 } 255 256 /** 257 * Convert a Wordpress DB User row to DokuWiki user info array 258 * and stores it in the users cache 259 * 260 * @param array $row Raw Wordpress user table row 261 * @return array user data 262 */ 263 protected function cacheUser($row) 264 { 265 global $conf; 266 267 $login = $row['user_login']; 268 269 // If the user is already cached, just return it 270 if (isset($this->users[$login])) { 271 return $this->users[$login]; 272 } 273 274 // Group membership - add DokuWiki's default group 275 $groups = array_keys(unserialize($row['grps'])); 276 if ($this->getConf('usedefaultgroup')) { 277 $groups[] = $conf['defaultgroup']; 278 } 279 280 $info = array( 281 'user' => $login, 282 'name' => $row['display_name'], 283 'pass' => $row['user_pass'], 284 'mail' => $row['user_email'], 285 'grps' => $groups, 286 ); 287 288 $this->users[$login] = $info; 289 return $info; 290 } 291 292 /** 293 * Loads all Wordpress users into the cache 294 * 295 * @return void 296 */ 297 protected function cacheAllUsers() 298 { 299 if ($this->usersCached) { 300 return; 301 } 302 303 $stmt = $this->db->prepare($this->sql_wp_user_data); 304 $stmt->execute(); 305 306 foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $user) { 307 $this->cacheUser($user); 308 } 309 310 $this->usersCached = true; 311 } 312 313 /** 314 * Build filter patterns from given criteria 315 * 316 * @param array $filter 317 */ 318 protected function setFilter($filter) 319 { 320 $this->filter = array(); 321 foreach ($filter as $field => $value) { 322 // Build PCRE pattern, utf8 + case insensitive 323 $this->filter[$field] = '/' . str_replace('/', '\/', $value) . '/ui'; 324 } 325 } 326 327 /** 328 * Return true if given user matches filter pattern, false otherwise 329 * 330 * @param string $user login 331 * @param array $info User data 332 * @return bool 333 */ 334 protected function applyFilter($user, $info) 335 { 336 foreach ($this->filter as $elem => $pattern) { 337 if ($elem == 'grps') { 338 if (!preg_grep($pattern, $info['grps'])) { 339 return false; 340 } 341 } else { 342 if (!preg_match($pattern, $info[$elem])) { 343 return false; 344 } 345 } 346 } 347 return true; 348 } 349} 350 351// vim:ts=4:sw=4:noet: 352