1<?php
2
3use dokuwiki\Extension\AuthPlugin;
4use dokuwiki\Logger;
5use dokuwiki\Utf8\Sort;
6
7class auth_plugin_authserversso extends AuthPlugin {
8	const CONF_VAR_AUTH_ID = 'auth_var_id';
9	const CONF_VAR_AUTH_EMAIL = 'auth_var_email';
10	const CONF_VAR_AUTH_REALNAME = 'auth_var_realname';
11	const CONF_AUTH_USERFILE = 'auth_userfile';
12
13	protected $users = null;
14
15	protected $pattern = array();
16
17	protected $globalConf = array();
18
19	public function __construct() {
20		parent::__construct();
21		if(!@is_readable($this->getConf(self::CONF_AUTH_USERFILE))) {
22			Logger::error("authserversso: Userfile not readable '{$this->getConf(self::CONF_AUTH_USERFILE)}'");
23			$this->success = false;
24		} else {
25			$this->cando['external'] = true;
26
27			if(@is_writable($this->getConf(self::CONF_AUTH_USERFILE))) {
28				Logger::debug("authserversso: Userfile is writable '{$this->getConf(self::CONF_AUTH_USERFILE)}'");
29				//$this->cando['addUser']   = true;
30				$this->cando['delUser']   = true;
31				//$this->cando['modLogin']  = false;
32				//$this->cando['modPass']   = false;
33				$this->cando['modMail']   = true;
34				$this->cando['modName']   = true;
35				$this->cando['modGroups'] = true;
36			}
37			$this->cando['getUsers']     = true;
38			$this->cando['getUserCount'] = true;
39			$this->cando['getGroups']    = true;
40		}
41
42		$this->loadConfig();
43		$this->success = true;
44	}
45
46	// Required
47	public function checkPass($user, $pass) {
48		Logger::debug("authserversso: checkPass '{$user}':'{$pass}' ");
49		return $this->trustExternal($user, $pass);
50	}
51
52	public function getUserData($user, $requireGroups=true) {
53		Logger::debug("authserversso: getUserData {$user}");
54		if($this->users === null) $this->loadUserData();
55		return $this->users[$user] ?? false;
56	}
57
58	protected function createUserLine($user, $pass, $name, $mail, $grps) {
59		$groups   = implode(',', $grps);
60		$userline = [$user, $pass, $name, $mail, $groups];
61		$userline = str_replace('\\', '\\\\', $userline); // escape \ as \\
62		$userline = str_replace(':', '\\:', $userline); // escape : as \:
63		$userline = str_replace('#', '\\#', $userline); // escape # as \#
64		$userline = implode(':', $userline)."\n";
65		return $userline;
66	}
67
68	public function createUser($user, $pwd, $name, $mail, $grps = null) {
69		global $conf;
70		Logger::debug("authserversso: createUser {$user}");
71
72		// user mustn't already exist
73		if($this->getUserData($user) !== false) {
74			msg($this->getLang('userexists'), -1);
75			return false;
76		}
77
78		$pass = auth_cryptPassword($pwd);
79
80		// set default group if no groups specified
81		if(!is_array($grps)) $grps = array($conf['defaultgroup']);
82
83		// prepare user line
84		$userline = $this->createUserLine($user, $pass, $name, $mail, $grps);
85
86		if(!io_saveFile($this->getConf(self::CONF_AUTH_USERFILE), $userline, true)) {
87			Logger::error($this->getLang('writefail'), -1);
88			return null;
89		}
90
91		$this->users[$user] = compact('pass', 'name', 'mail', 'grps');
92		return $pwd;
93	}
94
95	public function modifyUser($user, $changes) {
96		global $ACT;
97		global $conf;
98		Logger::debug("authserversso: modifyUser {$user}");
99
100		// sanity checks, user must already exist and there must be something to change
101		if(($userinfo = $this->getUserData($user)) === false) {
102			msg($this->getLang('usernotexists'), -1);
103			return false;
104		}
105
106		// don't modify protected users
107		if(!empty($userinfo['protected'])) {
108			msg(sprintf($this->getLang('protected'), hsc($user)), -1);
109			return false;
110		}
111
112		if(!is_array($changes) || !count($changes)) return true;
113
114		// update userinfo with new data, remembering to encrypt any password
115		$newuser = $user;
116		foreach($changes as $field => $value) {
117			if($field == 'user') {
118				$newuser = $value;
119				continue;
120			}
121			if($field == 'pass') $value = auth_cryptPassword($value);
122			$userinfo[$field] = $value;
123		}
124
125		$userline = $this->createUserLine($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $userinfo['grps']);
126
127		if(!io_replaceInFile($this->getConf(self::CONF_AUTH_USERFILE), '/^'.$user.':/', $userline, true)) {
128			msg('There was an error modifying your user data. You may need to register again.', -1);
129			// FIXME, io functions should be fail-safe so existing data isn't lost
130			$ACT = 'register';
131			return false;
132		}
133
134		$this->users[$newuser] = $userinfo;
135		return true;
136	}
137
138	public function deleteUsers($users) {
139		if(!is_array($users) || empty($users)) return 0;
140		Logger::debug('authserversso: deleteUsers');
141
142		if($this->users === null) $this->loadUserData();
143
144		$deleted = array();
145		foreach($users as $user) {
146			// don't delete protected users
147			if(!empty($this->users[$user]['protected'])) {
148				msg(sprintf($this->getLang('protected'), hsc($user)), -1);
149				continue;
150			}
151			if(isset($this->users[$user])) $deleted[] = preg_quote($user, '/');
152		}
153
154		if(empty($deleted)) return 0;
155
156		$pattern = '/^('.join('|', $deleted).'):/';
157		if (!io_deleteFromFile($this->getConf(self::CONF_AUTH_USERFILE), $pattern, true)) {
158			msg($this->getLang('writefail'), -1);
159			return 0;
160		}
161
162		// reload the user list and count the difference
163		$count = count($this->users);
164		$this->loadUserData();
165		$count -= count($this->users);
166		return $count;
167	}
168
169	public function getUserCount($filter = array()) {
170		//Logger::debug('authserversso: getUserCount');
171		if($this->users === null) $this->loadUserData();
172
173		if(!count($filter)) return count($this->users);
174
175		$count = 0;
176		$this->constructPattern($filter);
177
178		foreach($this->users as $user => $info) {
179			$count += $this->filter($user, $info);
180		}
181
182		return $count;
183	}
184
185	public function retrieveUsers($start = 0, $limit = 0, $filter = array()) {
186		//Logger::debug('authserversso: retrieveUsers');
187		if($this->users === null) $this->loadUserData();
188
189		Sort::ksort($this->users);
190
191		$i     = 0;
192		$count = 0;
193		$out   = [];
194		$this->constructPattern($filter);
195
196		foreach($this->users as $user => $info) {
197			if($this->filter($user, $info)) {
198				if($i >= $start) {
199					$out[$user] = $info;
200					$count++;
201					if(($limit > 0) && ($count >= $limit)) break;
202				}
203				$i++;
204			}
205		}
206
207		return $out;
208	}
209
210	public function retrieveGroups($start = 0, $limit = 0)
211    {
212        $groups = [];
213
214        if ($this->users === null) $this->loadUserData();
215        foreach ($this->users as $info) {
216            $groups = array_merge($groups, array_diff($info['grps'], $groups));
217        }
218        Sort::ksort($groups);
219
220        if ($limit > 0) {
221            return array_splice($groups, $start, $limit);
222        }
223        return array_splice($groups, $start);
224    }
225
226	public function cleanUser($user) {
227		global $conf;
228		return cleanID(str_replace(':', $conf['sepchar'], $user));
229	}
230
231	public function cleanGroup($group) {
232		global $conf;
233		return cleanID(str_replace(':', $conf['sepchar'], $group));
234	}
235
236	protected function loadUserData(){
237		//Logger::debug('authserversso: load user data');
238		$this->users = $this->readUserFile($this->getConf(self::CONF_AUTH_USERFILE));
239	}
240
241	protected function readUserFile($file) {
242		$users = array();
243		if(!file_exists($file)) return $users;
244
245		Logger::debug('authserversso: read user file');
246		$lines = file($file);
247		foreach($lines as $line) {
248			$line = preg_replace('/#.*$/', '', $line); //ignore comments
249			$line = trim($line);
250			if(empty($line)) continue;
251
252			$row = $this->splitUserData($line);
253			$row = str_replace('\\:', ':', $row);
254			$row = str_replace('\\\\', '\\', $row);
255
256			$groups = array_values(array_filter(explode(",", $row[4])));
257
258			$users[$row[0]]['pass'] = $row[1];
259			$users[$row[0]]['name'] = urldecode($row[2]);
260			$users[$row[0]]['mail'] = $row[3];
261			$users[$row[0]]['grps'] = $groups;
262		}
263		return $users;
264	}
265	protected function splitUserData($line){
266		$row = preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5);       // allow for : escaped as \:
267
268		if (count($row) < 5) {
269		    $row = array_pad($row, 5, '');
270			Logger::error('User row with less than 5 fields', $row);
271		}
272
273		return $row;
274	}
275
276	protected function filter($user, $info) {
277		foreach($this->pattern as $item => $pattern) {
278			if($item == 'user') {
279				if(!preg_match($pattern, $user)) return false;
280			} else if($item == 'grps') {
281				if(!count(preg_grep($pattern, $info['grps']))) return false;
282			} else {
283				if(!preg_match($pattern, $info[$item])) return false;
284			}
285		}
286		return true;
287	}
288
289	protected function constructPattern($filter) {
290		$this->pattern = array();
291		foreach($filter as $item => $pattern) {
292			$this->pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
293		}
294	}
295
296	/**
297	* Do all authentication
298	* @param   string  $user    Username
299	* @param   string  $pass    Cleartext Password
300	* @param   bool    $sticky  Cookie should not expire
301	* @return  bool             true on successful auth
302	*/
303	function trustExternal($user, $pass, $sticky=false) {
304		global $USERINFO;
305		global $ACT;
306		global $conf;
307		global $auth;
308
309		//Got a session already ?
310		if($this->hasSession()) {
311			//Logger::debug('authserversso: Session found');
312			return true;
313		}
314
315		Logger::debug('authserversso: trustExternal: No Session');
316
317		$userSso = $this->cleanUser($this->getSSOId());
318		$data = $this->getUserData($userSso);
319		if($data == false) {
320			Logger::debug('authserversso: trustExternal: user does not exist');
321			$mail = $this->getSSOMail();
322			$name = $this->getSSOName();
323			$pwd = auth_pwgen();
324			$pwd = $this->createUser($userSso, $pwd, $name, $mail);
325			if(!is_null($pwd)) {
326				$data = $this->getUserData($userSso);
327			}
328		}
329		if($data == false) {
330			Logger::debug('authserversso: trustExternal: could not get user');
331			return false;
332		}
333		$this->setSession($userSso, $data['grps'], $data['mail'], $data['name']);
334		//Logger::debug('authserversso: authenticated user');
335		return true;
336	}
337
338	private function getSSOId() {
339		return $this->getServerVar($this->getConf(self::CONF_VAR_AUTH_ID));
340	}
341
342	private function getSSOMail() {
343		$mail = $this->getServerVar($this->getConf(self::CONF_VAR_AUTH_EMAIL));
344		if(!$mail || !mail_isvalid($mail)) return null;
345		return $mail;
346	}
347
348	private function getSSOName() {
349		return $this->getServerVar($this->getConf(self::CONF_VAR_AUTH_REALNAME));
350	}
351
352	private function getServerVar($varName) {
353			if(is_null($varName)) return null;
354			if(!array_key_exists($varName, $_SERVER)) return null;
355			$varVal = $_SERVER[$varName];
356			Logger::debug("authserversso: getServerVar {$varName}:{$varVal}");
357			return $varVal;
358	}
359
360	private function hasSession() {
361			global $USERINFO;
362			//Logger::debug('authserversso: check hasSession');
363			if(!empty($_SESSION[DOKU_COOKIE]['auth']['info'])) {
364				//Logger::debug('authserversso: Session found');
365				$USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['info']['name'];
366				$USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['info']['mail'];
367				$USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['info']['grps'];
368				$_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user'];
369			}
370			return false;
371	}
372
373	// Create user session
374	private function setSession($user, $grps, $mail, $name) {
375			global $USERINFO;
376			$USERINFO['name'] = $name;
377			$USERINFO['mail'] = $mail;
378			$USERINFO['grps'] = is_array($grps) ? $grps : array();
379			$_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
380			$_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
381			$_SERVER['REMOTE_USER'] = $user;
382			return $_SESSION[DOKU_COOKIE];
383	}
384}