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