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