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