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
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',
48			'email'  => 'contact@h6e.net',
49			'date'   => '2011-02-15',
50			'name'   => 'OpenID plugin',
51			'desc'   => 'Authenticate on a DokuWiki with OpenID',
52			'url'    => 'http://h6e.net/dokuwiki/plugins/openid',
53		);
54	}
55
56	/**
57	 * Register the eventhandlers
58	 */
59	function register(&$controller)
60	{
61		$controller->register_hook('HTML_LOGINFORM_OUTPUT',
62			'BEFORE',
63			$this,
64			'handle_login_form',
65			array());
66		$controller->register_hook('HTML_UPDATEPROFILEFORM_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		$user = $_SERVER['REMOTE_USER'];
131
132		// Do not ask the user a password he didn't set
133		if ($event->data == 'profile') {
134			$conf['profileconfirm'] = 0;
135			if (preg_match('!^https?://!', $user)) {
136				$this->_redirect( $this->_self('openid') );
137			}
138		}
139
140		if ($event->data != 'openid' && $event->data != 'logout') {
141			// Warn the user to register an account if he's using a not registered OpenID
142			// and if registration is possible
143			if (preg_match('!^https?://!', $user)) {
144				if ($auth && $auth->canDo('addUser') && actionOK('register')) {
145					$message = sprintf($this->getLang('complete_registration_notice'), $this->_self('openid'));
146					msg($message, 2);
147				}
148			}
149		}
150
151		if ($event->data == 'openid') {
152
153			// not sure this if it's useful there
154			$event->stopPropagation();
155			$event->preventDefault();
156
157			if (isset($_POST['mode']) && ($_POST['mode'] == 'login' || $_POST['mode'] == 'add')) {
158
159				// we try to login with the OpenID submited
160				$consumer = $this->getConsumer();
161				$auth = $consumer->begin($_POST['openid_identifier']);
162				if (!$auth) {
163					msg($this->getLang('enter_valid_openid_error'), -1);
164					return;
165				}
166
167				// add an attribute query extension if we've never seen this OpenID before.
168				$associations = $this->get_associations();
169				if (!isset($associations[$openid])) {
170					require_once('Auth/OpenID/SReg.php');
171					$e = Auth_OpenID_SRegRequest::build(array(),array('nickname','email','fullname'));
172					$auth->addExtension($e);
173				}
174
175				// redirect to OpenID provider for authentication
176
177				// this fix an issue with mod_rewrite with JainRain library
178				// when a parameter seems to be non existing in the query
179				$return_to = $this->_self('openid') . '&id=' . $ID;
180
181				$url = $auth->redirectURL(DOKU_URL, $return_to);
182				$this->_redirect($url);
183
184			} else if (isset($_POST['mode']) && $_POST['mode'] == 'extra') {
185				// we register the user on the wiki and associate the account with his OpenID
186				$this->register_user();
187
188			} else if (isset($_POST['mode']) && $_POST['mode'] == 'delete') {
189				foreach ($_POST['delete'] as $identity => $state) {
190					$this->remove_openid_association($user, $identity);
191				}
192
193			} else if ($_GET['openid_mode'] == 'id_res') {
194				$consumer = $this->getConsumer();
195				$response = $consumer->complete($this->_self('openid'));
196				// set session variable depending on authentication result
197				if ($response->status == Auth_OpenID_SUCCESS) {
198
199					$openid = isset($_GET['openid1_claimed_id']) ? $_GET['openid1_claimed_id'] : $_GET['openid_claimed_id'];
200					if (empty($openid)) {
201						msg("Can't find OpenID claimed ID.", -1);
202						return false;
203					}
204
205					if (isset($user) && !preg_match('!^https?://!', $user)) {
206						$result = $this->register_openid_association($user, $openid);
207						if ($result) {
208							msg($this->getLang('openid_identity_added'), 1);
209						}
210					} else {
211						$authenticate = $this->login_user($openid);
212						if ($authenticate) {
213							// redirect to the page itself (without do=openid)
214							$this->_redirect(wl($ID));
215						}
216					}
217
218				} else {
219					msg($this->getLang('openid_authentication_failed') . ': ' . $response->message, -1);
220					return;
221				}
222
223			} else if ($_GET['openid_mode'] == 'cancel') {
224				// User cancelled the authentication
225				msg($this->getLang('openid_authentication_canceled'), 0);
226				return; // fall through to what ever action was called
227			}
228
229		}
230
231		return; // fall through to what ever action was called
232	}
233
234	/**
235	 * Create the OpenID login/complete forms
236	 */
237	function handle_act_unknown(&$event, $param)
238	{
239		global $auth, $ID;
240
241		if ($event->data != 'openid') {
242			return;
243		}
244
245		$event->stopPropagation();
246		$event->preventDefault();
247
248		$user = $_SERVER['REMOTE_USER'];
249
250		if (empty($user)) {
251			print $this->plugin_locale_xhtml('intro');
252			print '<div class="centeralign">'.NL;
253			$form = $this->get_openid_form('login');
254			html_form('register', $form);
255			print '</div>'.NL;
256		} else if (preg_match('!^https?://!', $user)) {
257			echo '<h1>', $this->getLang('openid_account_fieldset'), '</h1>', NL;
258			if ($auth && $auth->canDo('addUser') && actionOK('register')) {
259				echo '<p>', $this->getLang('openid_complete_text'), '</p>', NL;
260				print '<div class="centeralign">'.NL;
261				$form = $this->get_openid_form('extra');
262				html_form('complete', $form);
263				print '</div>'.NL;
264			} else {
265				echo '<p>', sprintf($this->getLang('openid_complete_disabled_text'), wl($ID)), '</p>', NL;
266			}
267		} else {
268			echo '<h1>', $this->getLang('openid_identities_title'), '</h1>', NL;
269			$identities = $this->get_associations($_SERVER['REMOTE_USER']);
270			if (!empty($identities)) {
271				echo '<form action="' . $this->_self('openid') . '" method="post"><div class="no">';
272				echo '<table>';
273				foreach ($identities as $identity => $user) {
274					echo '<tr>';
275					echo '<td width="10"><input type="checkbox" name="delete[' . htmlspecialchars($identity) . ']"/></td>';
276					echo '<td>' . $identity . '</td>';
277					echo '</tr>';
278				}
279				echo '</table>';
280				echo '<input type="hidden" name="mode" value="delete"/>';
281				echo '<input type="submit" value="' . $this->getLang('delete_selected_button') . '" class="button" />';
282				echo '</div></form>';
283			} else {
284				echo '<p>' . $this->getLang('none') . '</p>';
285			}
286			echo '<h1>' . $this->getLang('add_openid_title') . '</h1>';
287			print '<div class="centeralign">'.NL;
288			$form = new Doku_Form('openid__login', script());
289			$form->addHidden('do', 'openid');
290			$form->addHidden('mode', 'add');
291			$form->addElement(
292				form_makeTextField(
293					'openid_identifier', isset($_POST['openid_identifier']) ? $_POST['openid_identifier'] : '',
294					$this->getLang('openid_url_label'), 'openid__url', 'block', array('size'=>'50')
295				)
296			);
297			$form->addElement(form_makeButton('submit', '', $this->getLang('add_button')));
298			html_form('add', $form);
299			print '</div>'.NL;
300		}
301	}
302
303	/**
304	 * Generate the OpenID login/complete forms
305	 */
306	function get_openid_form($mode)
307	{
308		global $USERINFO, $lang;
309
310		$c = 'block';
311		$p = array('size'=>'50');
312
313		$form = new Doku_Form('openid__login', script());
314		$form->addHidden('id', $_GET['id']);
315		$form->addHidden('do', 'openid');
316		if ($mode == 'extra') {
317			$form->startFieldset($this->getLang('openid_account_fieldset'));
318			$form->addHidden('mode', 'extra');
319			$form->addElement(form_makeTextField('nickname', $_REQUEST['nickname'], $lang['user'], null, $c, $p));
320			$form->addElement(form_makeTextField('email', $_REQUEST['email'], $lang['email'], '', $c, $p));
321			$form->addElement(form_makeTextField('fullname', $_REQUEST['fullname'], $lang['fullname'], '', $c, $p));
322			$form->addElement(form_makeButton('submit', '', $this->getLang('complete_button')));
323		} else {
324			$form->startFieldset($this->getLang('openid_login_fieldset'));
325			$form->addHidden('mode', 'login');
326			$form->addElement(form_makeTextField('openid_identifier', $_REQUEST['openid_identifier'], $this->getLang('openid_url_label'), 'openid__url', $c, $p));
327			$form->addElement(form_makeButton('submit', '', $lang['btn_login']));
328		}
329		$form->endFieldset();
330		return $form;
331	}
332
333	/**
334	 * Insert link to OpenID into usual login form
335	 */
336	function handle_login_form(&$event, $param)
337	{
338		$msg = $this->getLang('login_link');
339		$msg = sprintf("<p>$msg</p>", $this->_self('openid'));
340		$pos = $event->data->findElementByAttribute('type', 'submit');
341		$event->data->insertElement($pos+2, $msg);
342	}
343
344	function handle_profile_form(&$event, $param)
345	{
346		echo '<p>', sprintf($this->getLang('manage_link'), $this->_self('openid')), '</p>';
347	}
348
349	/**
350	* Gets called when a OpenID login was succesful
351	*
352	* We store available userinfo in Session and Cookie
353	*/
354	function login_user($openid)
355	{
356		global $USERINFO, $auth, $conf;
357
358		// look for associations passed from an auth backend in user infos
359		$users = $auth->retrieveUsers();
360		foreach ($users as $id => $user) {
361			if (isset($user['openids'])) {
362				foreach ($user['openids'] as $identity) {
363					if ($identity == $openid) {
364						return $this->update_session($id);
365					}
366				}
367			}
368		}
369
370		$associations = $this->get_associations();
371
372		// this openid is associated with a real wiki user account
373		if (isset($associations[$openid])) {
374			$user = $associations[$openid];
375			return $this->update_session($user);
376		}
377
378		// no real wiki user account associated
379
380		// note that the generated cookie is invalid and will be invalided
381		// when the 'auth_security_timeout' expire
382		$this->update_session($openid);
383
384		$redirect_url = $this->_self('openid');
385
386		$sregs = array('email', 'nickname', 'fullname');
387		foreach ($sregs as $sreg) {
388			if (!empty($_GET["openid_sreg_$sreg"])) {
389				$redirect_url .= "&$sreg=" . urlencode($_GET["openid_sreg_$sreg"]);
390			}
391		}
392
393		// we will advice the user to register a real user account
394		$this->_redirect($redirect_url);
395	}
396
397	/**
398	 * Register the user in DokuWiki user conf,
399	 * write the OpenID association in the OpenID conf
400	 */
401	function register_user()
402	{
403		global $ID, $lang, $conf, $auth, $openid_associations;
404
405		if(!$auth->canDo('addUser')) return false;
406
407		$_POST['login'] = $_POST['nickname'];
408
409		// clean username
410		$_POST['login'] = preg_replace('/.*:/','',$_POST['login']);
411		$_POST['login'] = cleanID($_POST['login']);
412		// clean fullname and email
413		$_POST['fullname'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/','',$_POST['fullname']));
414		$_POST['email']    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/','',$_POST['email']));
415
416		if (empty($_POST['login']) || empty($_POST['fullname']) || empty($_POST['email'])) {
417			msg($lang['regmissing'], -1);
418			return false;
419		} else if (!mail_isvalid($_POST['email'])) {
420			msg($lang['regbadmail'], -1);
421			return false;
422		}
423
424		// okay try to create the user
425		if (!$auth->createUser($_POST['login'], auth_pwgen(), $_POST['fullname'], $_POST['email'])) {
426			msg($lang['reguexists'], -1);
427			return false;
428		}
429
430		$user = $_POST['login'];
431		$openid = $_SERVER['REMOTE_USER'];
432
433		// we update the OpenID associations array
434		$this->register_openid_association($user, $openid);
435
436		$this->update_session($user);
437
438		// account created, everything OK
439		$this->_redirect(wl($ID));
440	}
441
442	/**
443	 * Update user sessions
444	 *
445	 * Note that this doesn't play well with DokuWiki 'auth_security_timeout' configuration.
446	 *
447	 * So, you better set it to an high value, like '60*60*24', the user being disconnected
448	 * in that case one day after authentication
449	 */
450	function update_session($user)
451	{
452		session_start();
453
454		global $USERINFO, $INFO, $conf, $auth;
455
456		$_SERVER['REMOTE_USER'] = $user;
457
458		$USERINFO = $auth->getUserData($user);
459		if (empty($USERINFO)) {
460			$USERINFO['pass'] = 'invalid';
461			$USERINFO['name'] = 'OpenID';
462			$USERINFO['grps'] = array($conf['defaultgroup'], 'openid');
463		}
464
465		$pass = PMA_blowfish_encrypt($USERINFO['pass'], auth_cookiesalt());
466		auth_setCookie($user, $pass, false);
467
468		// auth data has changed, reinit the $INFO array
469		$INFO = pageinfo();
470
471		return true;
472	}
473
474	function register_openid_association($user, $openid)
475	{
476		$associations = $this->get_associations();
477		if (isset($associations[$openid])) {
478			msg($this->getLang('openid_already_user_error'), -1);
479			return false;
480		}
481		$associations[$openid] = $user;
482		$this->write_openid_associations($associations);
483		return true;
484	}
485
486	function remove_openid_association($user, $openid)
487	{
488		$associations = $this->get_associations();
489		if (isset($associations[$openid]) && $associations[$openid] == $user) {
490			unset($associations[$openid]);
491			$this->write_openid_associations($associations);
492			return true;
493		}
494		return false;
495	}
496
497	function write_openid_associations($associations)
498	{
499		$cfg = '<?php' . "\n";
500		foreach ($associations as $id => $login) {
501			$cfg .= '$openid_associations["' . addslashes($id) . '"] = "' . addslashes($login) . '"' . ";\n";
502		}
503		file_put_contents(DOKU_CONF.'openid.php', $cfg);
504		$this->openid_associations = $associations;
505	}
506
507	function get_associations($username = null)
508	{
509		if (isset($this->openid_associations)) {
510			$openid_associations = $this->openid_associations;
511		} else if (file_exists(DOKU_CONF.'openid.php')) {
512			// load OpenID associations array
513			$openid_associations = array();
514			include(DOKU_CONF.'openid.php');
515			$this->openid_associations = $openid_associations;
516		} else {
517			$this->openid_associations = $openid_associations = $openid_associations = array();
518		}
519		// Maybe is there a better way to filter the array
520		if (!empty($username)) {
521			$user_openid_associations = array();
522			foreach ((array)$openid_associations as $openid => $login) {
523				if ($username == $login) {
524					$user_openid_associations[$openid] = $login;
525				}
526			}
527			return $user_openid_associations;
528		}
529		return $openid_associations;
530	}
531
532}
533