124cd6f55SDamien Regad<?php 2*e03e23f3SDamien Regad 324cd6f55SDamien Regad/** 424cd6f55SDamien Regad * DokuWiki Plugin authwordpress (Auth Component) 524cd6f55SDamien Regad * 635dd80b8SDamien Regad * Provides authentication against a WordPress MySQL database backend 735dd80b8SDamien Regad * 835dd80b8SDamien Regad * This program is free software; you can redistribute it and/or modify 935dd80b8SDamien Regad * it under the terms of the GNU General Public License as published by 1035dd80b8SDamien Regad * the Free Software Foundation; version 2 of the License 1135dd80b8SDamien Regad * 1235dd80b8SDamien Regad * This program is distributed in the hope that it will be useful, 1335dd80b8SDamien Regad * but WITHOUT ANY WARRANTY; without even the implied warranty of 1435dd80b8SDamien Regad * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1535dd80b8SDamien Regad * GNU General Public License for more details. 1635dd80b8SDamien Regad * 1735dd80b8SDamien Regad * See the COPYING file in your DokuWiki folder for details 1835dd80b8SDamien Regad * 1924cd6f55SDamien Regad * @author Damien Regad <dregad@mantisbt.org> 2035dd80b8SDamien Regad * @copyright 2015 Damien Regad 2135dd80b8SDamien Regad * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 22b5b72c15SDamien Regad * @version 1.1 2335dd80b8SDamien Regad * @link https://github.com/dregad/dokuwiki-authwordpress 24d7435096SDamien Regad * 25d7435096SDamien Regad * @noinspection PhpComposerExtensionStubsInspection 26d7435096SDamien Regad * PhpUnused 27d7435096SDamien Regad * PhpMissingReturnTypeInspection 2824cd6f55SDamien Regad */ 2924cd6f55SDamien Regad 3024cd6f55SDamien Regad// must be run within Dokuwiki 31b741ff68SDamien Regadif (!defined('DOKU_INC')) { 32b741ff68SDamien Regad die(); 33b741ff68SDamien Regad} 3424cd6f55SDamien Regad 35308b48d3SDamien Regaduse dokuwiki\Logger; 36308b48d3SDamien Regad 3735dd80b8SDamien Regad/** 3835dd80b8SDamien Regad * WordPress password hashing framework 3935dd80b8SDamien Regad */ 4035dd80b8SDamien Regadrequire_once('class-phpass.php'); 4135dd80b8SDamien Regad 4235dd80b8SDamien Regad/** 4335dd80b8SDamien Regad * Authentication class 4435dd80b8SDamien Regad */ 45b741ff68SDamien Regad// @codingStandardsIgnoreLine 46b741ff68SDamien Regadclass auth_plugin_authwordpress extends DokuWiki_Auth_Plugin 47b741ff68SDamien Regad{ 4835dd80b8SDamien Regad /** 4935dd80b8SDamien Regad * SQL statement to retrieve User data from WordPress DB 5035dd80b8SDamien Regad * (including group memberships) 51b5b72c15SDamien Regad * '%prefix%' will be replaced by the actual prefix (from plugin config) 525d099ac1SDamien Regad * @var string $sql_wp_user_data 5335dd80b8SDamien Regad */ 540341717fSDamien Regad protected $sql_wp_user_data = "SELECT 5535dd80b8SDamien Regad id, user_login, user_pass, user_email, display_name, 56493dbdc6SFerdinand Thiessen meta_value AS grps 57b5b72c15SDamien Regad FROM %prefix%users u 582ef3e6a1SDamien Regad JOIN %prefix%usermeta m ON u.id = m.user_id AND meta_key = '%prefix%capabilities'"; 5924cd6f55SDamien Regad 6024cd6f55SDamien Regad /** 61015f33b2SDamien Regad * Wordpress database connection 625d099ac1SDamien Regad * @var PDO $db 63015f33b2SDamien Regad */ 640341717fSDamien Regad protected $db; 65015f33b2SDamien Regad 66d35aa3ecSDamien Regad /** 67d35aa3ecSDamien Regad * Users cache 685d099ac1SDamien Regad * @var array $users 69d35aa3ecSDamien Regad */ 70d35aa3ecSDamien Regad protected $users; 71d35aa3ecSDamien Regad 72d35aa3ecSDamien Regad /** 73d35aa3ecSDamien Regad * True if all users have been loaded in the cache 74d35aa3ecSDamien Regad * @see $users 755d099ac1SDamien Regad * @var bool $usersCached 76d35aa3ecSDamien Regad */ 77d35aa3ecSDamien Regad protected $usersCached = false; 78d35aa3ecSDamien Regad 799f5692a3SDamien Regad /** 809f5692a3SDamien Regad * Filter pattern 815d099ac1SDamien Regad * @var array $filter 829f5692a3SDamien Regad */ 839f5692a3SDamien Regad protected $filter; 84015f33b2SDamien Regad 85015f33b2SDamien Regad /** 8624cd6f55SDamien Regad * Constructor. 8724cd6f55SDamien Regad */ 88b741ff68SDamien Regad public function __construct() 89b741ff68SDamien Regad { 9035dd80b8SDamien Regad parent::__construct(); 9124cd6f55SDamien Regad 92aa69c0cfSDamien Regad // Plugin capabilities 9316ceb666SDamien Regad $this->cando['getUsers'] = true; 94aa69c0cfSDamien Regad $this->cando['getUserCount'] = true; 9516ceb666SDamien Regad 9635dd80b8SDamien Regad // Try to establish a connection to the WordPress DB 9735dd80b8SDamien Regad // abort in case of failure 9835dd80b8SDamien Regad try { 99b741ff68SDamien Regad $this->connectWordpressDb(); 100b741ff68SDamien Regad } catch (Exception $e) { 10135dd80b8SDamien Regad msg(sprintf($this->getLang('error_connect_failed'), $e->getMessage())); 10235dd80b8SDamien Regad $this->success = false; 10335dd80b8SDamien Regad return; 10435dd80b8SDamien Regad } 10524cd6f55SDamien Regad 106b5b72c15SDamien Regad // Initialize SQL query with configured prefix 107b5b72c15SDamien Regad $this->sql_wp_user_data = str_replace( 108b5b72c15SDamien Regad '%prefix%', 109b5b72c15SDamien Regad $this->getConf('prefix'), 110b5b72c15SDamien Regad $this->sql_wp_user_data 111b5b72c15SDamien Regad ); 112b5b72c15SDamien Regad 11324cd6f55SDamien Regad $this->success = true; 11424cd6f55SDamien Regad } 11524cd6f55SDamien Regad 11624cd6f55SDamien Regad 11724cd6f55SDamien Regad /** 118d7435096SDamien Regad * Check user+password. 11924cd6f55SDamien Regad * 12024cd6f55SDamien Regad * @param string $user the user name 12124cd6f55SDamien Regad * @param string $pass the clear text password 122d7435096SDamien Regad * 12324cd6f55SDamien Regad * @return bool 12435dd80b8SDamien Regad * 12535dd80b8SDamien Regad * @uses PasswordHash::CheckPassword WordPress password hasher 12624cd6f55SDamien Regad */ 127b741ff68SDamien Regad public function checkPass($user, $pass) 128b741ff68SDamien Regad { 12935dd80b8SDamien Regad $data = $this->getUserData($user); 13035dd80b8SDamien Regad if ($data === false) { 13135dd80b8SDamien Regad return false; 13224cd6f55SDamien Regad } 13324cd6f55SDamien Regad 13435dd80b8SDamien Regad $hasher = new PasswordHash(8, true); 135eed09871SDamien Regad $check = $hasher->CheckPassword($pass, $data['pass']); 136308b48d3SDamien Regad $this->logDebug("Password " . ($check ? 'OK' : 'Invalid')); 137eed09871SDamien Regad 138eed09871SDamien Regad return $check; 13935dd80b8SDamien Regad } 14035dd80b8SDamien Regad 14116ceb666SDamien Regad /** 142d7435096SDamien Regad * Bulk retrieval of user data. 14316ceb666SDamien Regad * 14416ceb666SDamien Regad * @param int $start index of first user to be returned 14516ceb666SDamien Regad * @param int $limit max number of users to be returned 14616ceb666SDamien Regad * @param array $filter array of field/pattern pairs 147d7435096SDamien Regad * 14816ceb666SDamien Regad * @return array userinfo (refer getUserData for internal userinfo details) 14916ceb666SDamien Regad */ 150b741ff68SDamien Regad public function retrieveUsers($start = 0, $limit = 0, $filter = array()) 151b741ff68SDamien Regad { 15216ceb666SDamien Regad msg($this->getLang('user_list_use_wordpress')); 1532ef3e6a1SDamien Regad 154d35aa3ecSDamien Regad $this->cacheAllUsers(); 1559f5692a3SDamien Regad 1569f5692a3SDamien Regad // Apply filter and pagination 1579f5692a3SDamien Regad $this->setFilter($filter); 1589f5692a3SDamien Regad $list = array(); 159acc20be0SDamien Regad $count = $i = 0; 1609f5692a3SDamien Regad foreach ($this->users as $user => $info) { 1619f5692a3SDamien Regad if ($this->applyFilter($user, $info)) { 1629f5692a3SDamien Regad if ($i >= $start) { 1639f5692a3SDamien Regad $list[$user] = $info; 1649f5692a3SDamien Regad $count++; 1659f5692a3SDamien Regad if ($limit > 0 && $count >= $limit) { 1669f5692a3SDamien Regad break; 1679f5692a3SDamien Regad } 1689f5692a3SDamien Regad } 1699f5692a3SDamien Regad $i++; 1709f5692a3SDamien Regad } 1719f5692a3SDamien Regad } 1729f5692a3SDamien Regad 1739f5692a3SDamien Regad return $list; 17416ceb666SDamien Regad } 17516ceb666SDamien Regad 176aa69c0cfSDamien Regad /** 177d7435096SDamien Regad * Return a count of the number of user which meet $filter criteria. 178aa69c0cfSDamien Regad * 179aa69c0cfSDamien Regad * @param array $filter 180d7435096SDamien Regad * 181aa69c0cfSDamien Regad * @return int 182aa69c0cfSDamien Regad */ 183b741ff68SDamien Regad public function getUserCount($filter = array()) 184b741ff68SDamien Regad { 185aa69c0cfSDamien Regad $this->cacheAllUsers(); 1869f5692a3SDamien Regad 1879f5692a3SDamien Regad if (empty($filter)) { 1889f5692a3SDamien Regad $count = count($this->users); 1899f5692a3SDamien Regad } else { 1909f5692a3SDamien Regad $this->setFilter($filter); 191acc20be0SDamien Regad $count = 0; 1929f5692a3SDamien Regad foreach ($this->users as $user => $info) { 1939f5692a3SDamien Regad $count += (int)$this->applyFilter($user, $info); 1949f5692a3SDamien Regad } 1959f5692a3SDamien Regad } 1969f5692a3SDamien Regad return $count; 197aa69c0cfSDamien Regad } 198aa69c0cfSDamien Regad 19935dd80b8SDamien Regad 20024cd6f55SDamien Regad /** 201d7435096SDamien Regad * Returns info about the given user. 20224cd6f55SDamien Regad * 20324cd6f55SDamien Regad * @param string $user the user name 2045d099ac1SDamien Regad * @param bool $requireGroups defaults to true 205d7435096SDamien Regad * 2065d099ac1SDamien Regad * @return array|false containing user data or false in case of error 20724cd6f55SDamien Regad */ 208b741ff68SDamien Regad public function getUserData($user, $requireGroups = true) 209b741ff68SDamien Regad { 210d35aa3ecSDamien Regad if (isset($this->users[$user])) { 211d35aa3ecSDamien Regad return $this->users[$user]; 212d35aa3ecSDamien Regad } 213d35aa3ecSDamien Regad 2142ef3e6a1SDamien Regad $sql = $this->sql_wp_user_data 2152ef3e6a1SDamien Regad . 'WHERE user_login = :user'; 21635dd80b8SDamien Regad 2172ef3e6a1SDamien Regad $stmt = $this->db->prepare($sql); 21835dd80b8SDamien Regad $stmt->bindParam(':user', $user); 219308b48d3SDamien Regad $this->logDebug("Retrieving data for user '$user'\n$sql"); 22035dd80b8SDamien Regad 22135dd80b8SDamien Regad if (!$stmt->execute()) { 2229520968dSDamien Regad // Query execution failed 223eed09871SDamien Regad $err = $stmt->errorInfo(); 224308b48d3SDamien Regad $this->logDebug("Error $err[1]: $err[2]"); 22524cd6f55SDamien Regad return false; 22624cd6f55SDamien Regad } 2279520968dSDamien Regad 2289520968dSDamien Regad $user = $stmt->fetch(PDO::FETCH_ASSOC); 2299520968dSDamien Regad if ($user === false) { 2309520968dSDamien Regad // Unknown user 231308b48d3SDamien Regad $this->logDebug("Unknown user"); 2329520968dSDamien Regad return false; 2339520968dSDamien Regad } 23424cd6f55SDamien Regad 235d35aa3ecSDamien Regad return $this->cacheUser($user); 23624cd6f55SDamien Regad } 23724cd6f55SDamien Regad 23824cd6f55SDamien Regad 23924cd6f55SDamien Regad /** 240d7435096SDamien Regad * Connect to Wordpress database. 241d7435096SDamien Regad * 242d7435096SDamien Regad * Initializes $db property as PDO object. 243d7435096SDamien Regad * 244d7435096SDamien Regad * @return void 24524cd6f55SDamien Regad */ 246d7435096SDamien Regad protected function connectWordpressDb(): void 247b741ff68SDamien Regad { 248cb81639bSDamien Regad if ($this->db) { 249cb81639bSDamien Regad // Already connected 250cb81639bSDamien Regad return; 251cb81639bSDamien Regad } 252cb81639bSDamien Regad 253cb81639bSDamien Regad // Build connection string 25435dd80b8SDamien Regad $dsn = array( 25535dd80b8SDamien Regad 'host=' . $this->getConf('hostname'), 25635dd80b8SDamien Regad 'dbname=' . $this->getConf('database'), 25702fbe6b4SDamien Regad 'charset=UTF8', 25835dd80b8SDamien Regad ); 25935dd80b8SDamien Regad $port = $this->getConf('port'); 26035dd80b8SDamien Regad if ($port) { 26135dd80b8SDamien Regad $dsn[] = 'port=' . $port; 26235dd80b8SDamien Regad } 26335dd80b8SDamien Regad $dsn = 'mysql:' . implode(';', $dsn); 26435dd80b8SDamien Regad 265015f33b2SDamien Regad $this->db = new PDO($dsn, $this->getConf('username'), $this->getConf('password')); 26624cd6f55SDamien Regad } 26724cd6f55SDamien Regad 268d1f83a80SDamien Regad /** 269d7435096SDamien Regad * Cache User Data. 270d7435096SDamien Regad * 271d1f83a80SDamien Regad * Convert a Wordpress DB User row to DokuWiki user info array 272d7435096SDamien Regad * and stores it in the users cache. 273d1f83a80SDamien Regad * 2745d099ac1SDamien Regad * @param array $row Raw Wordpress user table row 275d7435096SDamien Regad * 276d1f83a80SDamien Regad * @return array user data 277d1f83a80SDamien Regad */ 278d7435096SDamien Regad protected function cacheUser(array $row): array 279b741ff68SDamien Regad { 280d1f83a80SDamien Regad global $conf; 281d1f83a80SDamien Regad 282d35aa3ecSDamien Regad $login = $row['user_login']; 283d35aa3ecSDamien Regad 284d35aa3ecSDamien Regad // If the user is already cached, just return it 285d35aa3ecSDamien Regad if (isset($this->users[$login])) { 286d35aa3ecSDamien Regad return $this->users[$login]; 287d35aa3ecSDamien Regad } 288d35aa3ecSDamien Regad 289d1f83a80SDamien Regad // Group membership - add DokuWiki's default group 290493dbdc6SFerdinand Thiessen $groups = array_keys(unserialize($row['grps'])); 291d1f83a80SDamien Regad if ($this->getConf('usedefaultgroup')) { 292d1f83a80SDamien Regad $groups[] = $conf['defaultgroup']; 293d1f83a80SDamien Regad } 294d1f83a80SDamien Regad 295d1f83a80SDamien Regad $info = array( 296d35aa3ecSDamien Regad 'user' => $login, 297d35aa3ecSDamien Regad 'name' => $row['display_name'], 298d35aa3ecSDamien Regad 'pass' => $row['user_pass'], 299d35aa3ecSDamien Regad 'mail' => $row['user_email'], 300d1f83a80SDamien Regad 'grps' => $groups, 301d1f83a80SDamien Regad ); 302d35aa3ecSDamien Regad 303d35aa3ecSDamien Regad $this->users[$login] = $info; 304d1f83a80SDamien Regad return $info; 305d1f83a80SDamien Regad } 306d1f83a80SDamien Regad 307d35aa3ecSDamien Regad /** 308d7435096SDamien Regad * Loads all Wordpress users into the cache. 309d35aa3ecSDamien Regad * 310d35aa3ecSDamien Regad * @return void 311d35aa3ecSDamien Regad */ 312b741ff68SDamien Regad protected function cacheAllUsers() 313b741ff68SDamien Regad { 314d35aa3ecSDamien Regad if ($this->usersCached) { 315d35aa3ecSDamien Regad return; 316d35aa3ecSDamien Regad } 317d35aa3ecSDamien Regad 318d35aa3ecSDamien Regad $stmt = $this->db->prepare($this->sql_wp_user_data); 319d35aa3ecSDamien Regad $stmt->execute(); 320d35aa3ecSDamien Regad 321d35aa3ecSDamien Regad foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $user) { 322d35aa3ecSDamien Regad $this->cacheUser($user); 323d35aa3ecSDamien Regad } 324d35aa3ecSDamien Regad 325d35aa3ecSDamien Regad $this->usersCached = true; 326d35aa3ecSDamien Regad } 327d35aa3ecSDamien Regad 3289f5692a3SDamien Regad /** 329d7435096SDamien Regad * Build filter patterns from given criteria. 3309f5692a3SDamien Regad * 3319f5692a3SDamien Regad * @param array $filter 332d7435096SDamien Regad * 333d7435096SDamien Regad * @return void 3349f5692a3SDamien Regad */ 335d7435096SDamien Regad protected function setFilter(array $filter): void 336b741ff68SDamien Regad { 3379f5692a3SDamien Regad $this->filter = array(); 3389f5692a3SDamien Regad foreach ($filter as $field => $value) { 3399f5692a3SDamien Regad // Build PCRE pattern, utf8 + case insensitive 3409f5692a3SDamien Regad $this->filter[$field] = '/' . str_replace('/', '\/', $value) . '/ui'; 3419f5692a3SDamien Regad } 3429f5692a3SDamien Regad } 3439f5692a3SDamien Regad 3449f5692a3SDamien Regad /** 345d7435096SDamien Regad * Return true if given user matches filter pattern, false otherwise. 3469f5692a3SDamien Regad * 3479f5692a3SDamien Regad * @param string $user login 3489f5692a3SDamien Regad * @param array $info User data 349d7435096SDamien Regad * 3509f5692a3SDamien Regad * @return bool 3519f5692a3SDamien Regad */ 352d7435096SDamien Regad protected function applyFilter(string $user, array $info): bool 353b741ff68SDamien Regad { 3549f5692a3SDamien Regad foreach ($this->filter as $elem => $pattern) { 3559f5692a3SDamien Regad if ($elem == 'grps') { 35637a3480aSDamien Regad if (!preg_grep($pattern, $info['grps'])) { 3579f5692a3SDamien Regad return false; 3589f5692a3SDamien Regad } 3599f5692a3SDamien Regad } else { 3609f5692a3SDamien Regad if (!preg_match($pattern, $info[$elem])) { 3619f5692a3SDamien Regad return false; 3629f5692a3SDamien Regad } 3639f5692a3SDamien Regad } 3649f5692a3SDamien Regad } 3659f5692a3SDamien Regad return true; 3669f5692a3SDamien Regad } 367308b48d3SDamien Regad 368308b48d3SDamien Regad /** 369308b48d3SDamien Regad * Add message to debug log. 370308b48d3SDamien Regad * 371308b48d3SDamien Regad * @param string $msg 372308b48d3SDamien Regad * 373308b48d3SDamien Regad * @return void 374308b48d3SDamien Regad */ 375308b48d3SDamien Regad protected function logDebug(string $msg): void 376308b48d3SDamien Regad { 377308b48d3SDamien Regad global $updateVersion; 378308b48d3SDamien Regad if ($updateVersion >= 52) { 379308b48d3SDamien Regad Logger::debug($msg); 380308b48d3SDamien Regad } else { 381308b48d3SDamien Regad dbglog($msg); 382308b48d3SDamien Regad } 383308b48d3SDamien Regad } 38424cd6f55SDamien Regad} 38524cd6f55SDamien Regad 3860e6cb03cSDamien Regad// vim:ts=4:sw=4:noet: 387