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
33 if(!defined('DOKU_INC')) die();
34 
35 if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
36 
37 require_once(DOKU_PLUGIN.'action.php');
38 
39 class 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