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