1<?php
2
3/**
4 * DokuWiki OpenID plugin
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     This version by François Hodierne (http://h6e.net/)
8 * @author     Original by Andreas Gohr <andi@splitbrain.org>
9 * @version    2.2.0-ul-2
10 */
11
12/**
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License version 2,
15 * as published by the Free Software Foundation.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 * GNU General Public License for more details.
21 *
22 * The license for this software can likely be found here:
23 * http://www.gnu.org/licenses/gpl-2.0.html
24 */
25
26/**
27 * This program also use the PHP OpenID library by JanRain, Inc.
28 * which is licensed under the Apache license 2.0:
29 * http://www.apache.org/licenses/LICENSE-2.0
30 */
31
32// must be run within Dokuwiki
33if(!defined('DOKU_INC')) die();
34
35if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
36
37require_once(DOKU_PLUGIN.'action.php');
38
39class action_plugin_openid extends DokuWiki_Action_Plugin {
40
41	/**
42	 * Return some info
43	 */
44	function getInfo()
45	{
46		return array(
47			'author' => 'h6e.net / 7usr7local / tzzee',
48			'email'  => 'pjw-git-2018@usr-local.org',
49			'date'   => '2023-02-18',
50			'name'   => 'OpenID plugin',
51			'desc'   => 'Authenticate on a DokuWiki with OpenID (Vers. 2.2.0-ul-2)',
52			'url'    => 'https://github.com/usr-local/dokuwiki-openid',
53		);
54	}
55
56	/**
57	 * Register the eventhandlers
58	 */
59	function register(Doku_Event_Handler $controller)
60	{
61		$controller->register_hook('FORM_LOGIN_OUTPUT',
62			'BEFORE',
63			$this,
64			'handle_login_form',
65			array());
66		$controller->register_hook('FORM_UPDATEPROFILE_OUTPUT',
67			'AFTER',
68			$this,
69			'handle_profile_form',
70			array());
71		$controller->register_hook('ACTION_ACT_PREPROCESS',
72			'BEFORE',
73			$this,
74			'handle_act_preprocess',
75			array());
76		$controller->register_hook('TPL_ACT_UNKNOWN',
77			'BEFORE',
78			$this,
79			'handle_act_unknown',
80			array());
81	}
82
83	/**
84	 * Returns the Consumer URL
85	 */
86	function _self($do)
87	{
88		global $ID;
89		return wl($ID, 'do=' . $do, true, '&');
90	}
91
92	/**
93	 * Redirect the user
94	 */
95	function _redirect($url)
96	{
97		header('Location: ' . $url);
98		exit;
99	}
100
101	/**
102	 * Return an OpenID Consumer
103	 */
104	function getConsumer()
105	{
106		global $conf;
107		if (isset($this->consumer)) {
108			return $this->consumer;
109		}
110		define('Auth_OpenID_RAND_SOURCE', null);
111		set_include_path( get_include_path() . PATH_SEPARATOR . dirname(__FILE__) );
112		require_once "Auth/OpenID/Consumer.php";
113		require_once "Auth/OpenID/FileStore.php";
114		// start session (needed for YADIS)
115		session_start();
116		// create file storage area for OpenID data
117		$store = new Auth_OpenID_FileStore($conf['tmpdir'] . '/openid');
118		// create OpenID consumer
119		$this->consumer = new Auth_OpenID_Consumer($store);
120		return $this->consumer;
121	}
122
123	/**
124	 * Handles the openid action
125	 */
126	function handle_act_preprocess(&$event, $param)
127	{
128		global $ID, $conf, $auth;
129
130		$disabled = explode(',', $conf['disableactions']);
131		if ($this->getConf('openid_disable_registration')) {
132			$disabled[] = 'register';
133		}
134		if ($this->getConf('openid_disable_update_profile')) {
135			$disabled[] = 'resendpwd';
136			$disabled[] = 'profile';
137		}
138		$conf['disableactions'] = implode(',', $disabled);
139
140		$user = $_SERVER['REMOTE_USER'];
141
142		// Do not ask the user a password he didn't set
143		if ($event->data == 'profile') {
144			$conf['profileconfirm'] = 0;
145			if (preg_match('!^https?://!', $user)) {
146				$this->_redirect( $this->_self('openid') );
147			}
148		}
149
150		if ($event->data != 'openid' && $event->data != 'logout') {
151			// Warn the user to register an account if he's using a not registered OpenID
152			// and if registration is possible
153			if (preg_match('!^https?://!', $user)) {
154				if ($auth && $auth->canDo('addUser') && actionOK('register')) {
155					$message = sprintf($this->getLang('complete_registration_notice'), $this->_self('openid'));
156					msg($message, 2);
157				}
158			}
159		}
160
161		if ($event->data == 'openid') {
162
163			// not sure this if it's useful there
164			$event->stopPropagation();
165			$event->preventDefault();
166
167			if (isset($_POST['mode']) && ($_POST['mode'] == 'login' || $_POST['mode'] == 'add')) {
168
169				// we try to login with the OpenID submited
170				$consumer = $this->getConsumer();
171				$auth = $consumer->begin($_POST['openid_identifier']);
172				if (!$auth) {
173					msg($this->getLang('enter_valid_openid_error'), -1);
174					return;
175				}
176
177				// add an attribute query extension if we've never seen this OpenID before.
178				$associations = $this->get_associations();
179				if (!isset($associations[$openid])) {
180					require_once('Auth/OpenID/SReg.php');
181					$e = Auth_OpenID_SRegRequest::build(array(),array('nickname','email','fullname'));
182					$auth->addExtension($e);
183				}
184
185				// redirect to OpenID provider for authentication
186
187				// this fix an issue with mod_rewrite with JainRain library
188				// when a parameter seems to be non existing in the query
189				$return_to = $this->_self('openid') . '&id=' . $ID;
190
191				$url = $auth->redirectURL(DOKU_URL, $return_to);
192				$this->_redirect($url);
193
194			} else if (isset($_POST['mode']) && $_POST['mode'] == 'extra') {
195				// we register the user on the wiki and associate the account with his OpenID
196				$this->register_user();
197
198			} else if (isset($_POST['mode']) && $_POST['mode'] == 'delete') {
199				foreach ($_POST['delete'] as $identity => $state) {
200					$this->remove_openid_association($user, $identity);
201				}
202
203			} else if ($_GET['openid_mode'] == 'id_res') {
204				$consumer = $this->getConsumer();
205				$response = $consumer->complete($this->_self('openid'));
206				// set session variable depending on authentication result
207				if ($response->status == Auth_OpenID_SUCCESS) {
208
209					$openid = isset($_GET['openid1_claimed_id']) ? $_GET['openid1_claimed_id'] : $_GET['openid_claimed_id'];
210					if (empty($openid)) {
211						msg("Can't find OpenID claimed ID.", -1);
212						return false;
213					}
214
215					if (isset($user) && !preg_match('!^https?://!', $user)) {
216						$result = $this->register_openid_association($user, $openid);
217						if ($result) {
218							msg($this->getLang('openid_identity_added'), 1);
219						}
220					} else {
221						$authenticate = $this->login_user($openid);
222						if ($authenticate) {
223							$log = array('message' => 'logged in temporarily', 'user' => $user);
224							trigger_event('PLUGIN_LOGLOG_LOG', $log);
225							// redirect to the page itself (without do=openid)
226							$this->_redirect(wl($ID));
227						}
228					}
229
230				} else {
231					msg($this->getLang('openid_authentication_failed') . ': ' . $response->message, -1);
232					$log = array('message' => 'failed login attempt', 'user' => $user);
233					trigger_event('PLUGIN_LOGLOG_LOG', $log);
234					return;
235				}
236
237			} else if ($_GET['openid_mode'] == 'cancel') {
238				// User cancelled the authentication
239				msg($this->getLang('openid_authentication_canceled'), 0);
240				return; // fall through to what ever action was called
241			}
242
243		}
244
245		if ($this->getConf('openid_disable_registration') && $event->data == 'register') {
246			$event->stopPropagation();
247			$event->preventDefault();
248			msg($this->getLang('openid_registration_denied'), -1);
249			return;
250		}
251		if ($this->getConf('openid_disable_update_profile') && ($event->data == 'profile'||$event->data == 'resendpwd')) {
252			$event->stopPropagation();
253			$event->preventDefault();
254			msg($this->getLang('openid_update_profile_denied'), -1);
255			return;
256		}
257
258		return; // fall through to what ever action was called
259	}
260
261	/**
262	 * Create the OpenID login/complete forms
263	 */
264	function handle_act_unknown(&$event, $param)
265	{
266		global $auth, $ID;
267
268		if ($event->data != 'openid') {
269			return;
270		}
271
272		$event->stopPropagation();
273		$event->preventDefault();
274
275		$user = $_SERVER['REMOTE_USER'];
276
277		if (empty($user)) {
278			print $this->locale_xhtml('intro');
279			print '<div class="centeralign">'.NL;
280			$form = $this->get_openid_form('login');
281			html_form('register', $form);
282			print '</div>'.NL;
283		} else if (preg_match('!^https?://!', $user)) {
284			echo '<h1>', $this->getLang('openid_account_fieldset'), '</h1>', NL;
285			if ($auth && $auth->canDo('addUser') && actionOK('register')) {
286				echo '<p>', $this->getLang('openid_complete_text'), '</p>', NL;
287				print '<div class="centeralign">'.NL;
288				$form = $this->get_openid_form('extra');
289				html_form('complete', $form);
290				print '</div>'.NL;
291			} else {
292				echo '<p>', sprintf($this->getLang('openid_complete_disabled_text'), wl($ID)), '</p>', NL;
293			}
294		} else if (!$this->getConf('openid_disable_update_profile')) {
295			echo '<h1>', $this->getLang('openid_identities_title'), '</h1>', NL;
296			$identities = $this->get_associations($_SERVER['REMOTE_USER']);
297			if (!empty($identities)) {
298				echo '<form action="' . $this->_self('openid') . '" method="post"><div class="no">';
299				echo '<table>';
300				foreach ($identities as $identity => $user) {
301					echo '<tr>';
302					echo '<td width="10"><input type="checkbox" name="delete[' . htmlspecialchars($identity) . ']"/></td>';
303					echo '<td>' . $identity . '</td>';
304					echo '</tr>';
305				}
306				echo '</table>';
307				echo '<input type="hidden" name="mode" value="delete"/>';
308				echo '<input type="submit" value="' . $this->getLang('delete_selected_button') . '" class="button" />';
309				echo '</div></form>';
310			} else {
311				echo '<p>' . $this->getLang('none') . '</p>';
312			}
313			echo '<h1>' . $this->getLang('add_openid_title') . '</h1>';
314			print '<div class="centeralign">'.NL;
315			$form = new Doku_Form('openid__login', script());
316			$form->addHidden('do', 'openid');
317			$form->addHidden('mode', 'add');
318			$form->addElement(
319				form_makeTextField(
320					'openid_identifier', isset($_POST['openid_identifier']) ? $_POST['openid_identifier'] : '',
321					$this->getLang('openid_url_label'), 'openid__url', 'block', array('size'=>'50')
322				)
323			);
324			$form->addElement(form_makeButton('submit', '', $this->getLang('add_button')));
325			html_form('add', $form);
326			print '</div>'.NL;
327		} else {
328			msg($this->getLang('openid_update_profile_denied'), -1);
329		}
330	}
331
332	/**
333	 * Generate the OpenID login/complete forms
334	 */
335	function get_openid_form($mode)
336	{
337		global $USERINFO, $lang;
338
339		$c = 'block';
340		$p = array('size'=>'50');
341
342		$form = new Doku_Form('openid__login', script());
343		$form->addHidden('id', $_GET['id']);
344		$form->addHidden('do', 'openid');
345		if ($mode == 'extra') {
346			$form->startFieldset($this->getLang('openid_account_fieldset'));
347			$form->addHidden('mode', 'extra');
348			if($this->getConf('openid_disable_update_profile')){
349				$form->addHidden('nickname', $_REQUEST['nickname']);
350				$form->addHidden('email', $_REQUEST['email']);
351				$form->addHidden('fullname', $_REQUEST['fullname']);
352			}else{
353				$form->addElement(form_makeTextField('nickname', $_REQUEST['nickname'], $lang['user'], null, $c, $p));
354				$form->addElement(form_makeTextField('email', $_REQUEST['email'], $lang['email'], '', $c, $p));
355				$form->addElement(form_makeTextField('fullname', $_REQUEST['fullname'], $lang['fullname'], '', $c, $p));
356			}
357			$form->addElement(form_makeButton('submit', '', $this->getLang('complete_button')));
358		} else {
359			$form->startFieldset($this->getLang('openid_login_fieldset'));
360			$form->addHidden('mode', 'login');
361			if (!empty($this->getConf('openid_identifier'))){
362				$form->addHidden('openid_identifier', $this->getConf('openid_identifier'));
363			}else{
364				$form->addElement(form_makeTextField('openid_identifier', $_REQUEST['openid_identifier'], $this->getLang('openid_url_label'), 'openid__url', $c, $p));
365			}
366			$form->addElement(form_makeButton('submit', '', $lang['btn_login']));
367		}
368		$form->endFieldset();
369		return $form;
370	}
371
372	/**
373	 * Insert link to OpenID into usual login form
374	 */
375	function handle_login_form(&$event, $param)
376	{
377		$msg = $this->getLang('login_link');
378		$msg = sprintf("<p>$msg</p>", $this->_self('openid'));
379		$pos = $event->data->findPositionByAttribute('type', 'submit');
380		$event->data->addHTML($msg, $pos+2);
381	}
382
383	function handle_profile_form(&$event, $param)
384	{
385		echo '<p>', sprintf($this->getLang('manage_link'), $this->_self('openid')), '</p>';
386	}
387
388	/**
389	* Gets called when a OpenID login was succesful
390	*
391	* We store available userinfo in Session and Cookie
392	*/
393	function login_user($openid)
394	{
395		global $USERINFO, $auth, $conf;
396
397		// look for associations passed from an auth backend in user infos
398		$users = $auth->retrieveUsers();
399		foreach ($users as $id => $user) {
400			if (isset($user['openids'])) {
401				foreach ($user['openids'] as $identity) {
402					if ($identity == $openid) {
403						return $this->update_session($id);
404					}
405				}
406			}
407		}
408
409		$associations = $this->get_associations();
410
411		// this openid is associated with a real wiki user account
412		if (isset($associations[$openid])) {
413			$user = $associations[$openid];
414			return $this->update_session($user);
415		}
416
417		// no real wiki user account associated
418
419		// note that the generated cookie is invalid and will be invalided
420		// when the 'auth_security_timeout' expire
421		$this->update_session($openid);
422
423		$redirect_url = $this->_self('openid');
424
425		$sregs = array('email', 'nickname', 'fullname');
426		foreach ($sregs as $sreg) {
427			if (!empty($_GET["openid_sreg_$sreg"])) {
428				$redirect_url .= "&$sreg=" . urlencode($_GET["openid_sreg_$sreg"]);
429			}
430		}
431
432		// we will advice the user to register a real user account
433		$this->_redirect($redirect_url);
434	}
435
436	/**
437	 * Register the user in DokuWiki user conf,
438	 * write the OpenID association in the OpenID conf
439	 */
440	function register_user()
441	{
442		global $ID, $lang, $conf, $auth, $openid_associations;
443
444		if(!$auth->canDo('addUser')) return false;
445
446		$_POST['login'] = $_POST['nickname'];
447
448		// clean username
449		$_POST['login'] = preg_replace('/.*:/','',$_POST['login']);
450		$_POST['login'] = cleanID($_POST['login']);
451		// clean fullname and email
452		$_POST['fullname'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/','',$_POST['fullname']));
453		$_POST['email']    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/','',$_POST['email']));
454
455		if (empty($_POST['login']) || empty($_POST['fullname']) || empty($_POST['email'])) {
456			msg($lang['regmissing'], -1);
457			return false;
458		} else if (!mail_isvalid($_POST['email'])) {
459			msg($lang['regbadmail'], -1);
460			return false;
461		}
462
463		// okay try to create the user
464		if (!$auth->createUser($_POST['login'], auth_pwgen(), $_POST['fullname'], $_POST['email'])) {
465			msg($lang['reguexists'], -1);
466			return false;
467		}
468
469		$user = $_POST['login'];
470		$openid = $_SERVER['REMOTE_USER'];
471
472		// we update the OpenID associations array
473		$this->register_openid_association($user, $openid);
474
475		$this->update_session($user);
476
477		$log = array('message' => 'logged in permanently', 'user' => $user);
478		trigger_event('PLUGIN_LOGLOG_LOG', $log);
479
480		// account created, everything OK
481		$this->_redirect(wl($ID));
482	}
483
484	/**
485	 * Update user sessions
486	 *
487	 * Note that this doesn't play well with DokuWiki 'auth_security_timeout' configuration.
488	 *
489	 * So, you better set it to an high value, like '60*60*24', the user being disconnected
490	 * in that case one day after authentication
491	 */
492	function update_session($user)
493	{
494		session_start();
495
496		global $USERINFO, $INFO, $conf, $auth;
497
498		$_SERVER['REMOTE_USER'] = $user;
499
500		$USERINFO = $auth->getUserData($user);
501		if (empty($USERINFO)) {
502			$USERINFO['pass'] = 'invalid';
503			$USERINFO['name'] = 'OpenID';
504			$USERINFO['grps'] = array($conf['defaultgroup'], 'openid');
505		}
506
507		$pass = auth_encrypt($USERINFO['pass'], auth_cookiesalt());
508		auth_setCookie($user, $pass, false);
509
510		// auth data has changed, reinit the $INFO array
511		$INFO = pageinfo();
512
513		return true;
514	}
515
516	function register_openid_association($user, $openid)
517	{
518		$associations = $this->get_associations();
519		if (isset($associations[$openid])) {
520			msg($this->getLang('openid_already_user_error'), -1);
521			return false;
522		}
523		$associations[$openid] = $user;
524		$this->write_openid_associations($associations);
525		return true;
526	}
527
528	function remove_openid_association($user, $openid)
529	{
530		$associations = $this->get_associations();
531		if (isset($associations[$openid]) && $associations[$openid] == $user) {
532			unset($associations[$openid]);
533			$this->write_openid_associations($associations);
534			return true;
535		}
536		return false;
537	}
538
539	function write_openid_associations($associations)
540	{
541		$cfg = '<?php' . "\n";
542		foreach ($associations as $id => $login) {
543			$cfg .= '$openid_associations["' . addslashes($id) . '"] = "' . addslashes($login) . '"' . ";\n";
544		}
545		file_put_contents(DOKU_CONF.'openid.php', $cfg);
546		$this->openid_associations = $associations;
547	}
548
549	function get_associations($username = null)
550	{
551		if (isset($this->openid_associations)) {
552			$openid_associations = $this->openid_associations;
553		} else if (file_exists(DOKU_CONF.'openid.php')) {
554			// load OpenID associations array
555			$openid_associations = array();
556			include(DOKU_CONF.'openid.php');
557			$this->openid_associations = $openid_associations;
558		} else {
559			$this->openid_associations = $openid_associations = $openid_associations = array();
560		}
561		// Maybe is there a better way to filter the array
562		if (!empty($username)) {
563			$user_openid_associations = array();
564			foreach ((array)$openid_associations as $openid => $login) {
565				if ($username == $login) {
566					$user_openid_associations[$openid] = $login;
567				}
568			}
569			return $user_openid_associations;
570		}
571		return $openid_associations;
572	}
573
574}
575