1fca58076SAndreas Gohr<?php 28b7620a8SAndreas Gohr 3fca58076SAndreas Gohr/** 4fca58076SAndreas Gohr * Two Factor Action Plugin 5fca58076SAndreas Gohr * 6fca58076SAndreas Gohr * @author Mike Wilmes mwilmes@avc.edu 7fca58076SAndreas Gohr * Big thanks to Daniel Popp and his Google 2FA code (authgoogle2fa) as a 8fca58076SAndreas Gohr * starting reference. 9fca58076SAndreas Gohr * 10fca58076SAndreas Gohr * Overview: 11fca58076SAndreas Gohr * The plugin provides for two opportunities to perform two factor 12fca58076SAndreas Gohr * authentication. The first is on the main login page, via a code provided by 13fca58076SAndreas Gohr * an external authenticator. The second is at a separate prompt after the 14fca58076SAndreas Gohr * initial login. By default, all modules will process from the second login, 15fca58076SAndreas Gohr * but a module can subscribe to accepting a password from the main login when 16fca58076SAndreas Gohr * it makes sense, because the user has access to the code in advance. 17fca58076SAndreas Gohr * 18fca58076SAndreas Gohr * If a user only has configured modules that provide for login at the main 19fca58076SAndreas Gohr * screen, the code will only be accepted at the main login screen for 20fca58076SAndreas Gohr * security purposes. 21fca58076SAndreas Gohr * 22fca58076SAndreas Gohr * Modules will be called to render their configuration forms on the profile 23fca58076SAndreas Gohr * page and to verify a user's submitted code. If any module accepts the 24fca58076SAndreas Gohr * submitted code, then the user is granted access. 25fca58076SAndreas Gohr * 26fca58076SAndreas Gohr * Each module may be used to transmit a message to the user that their 27fca58076SAndreas Gohr * account has been logged into. One module may be used as the default 28fca58076SAndreas Gohr * transmit option. These options are handled by the parent module. 29fca58076SAndreas Gohr */ 30fca58076SAndreas Gohr 31fca58076SAndreas Gohr// Create a definition for a 2FA cookie. 328b7620a8SAndreas Gohruse dokuwiki\plugin\twofactor\Manager; 338b7620a8SAndreas Gohr 34fca58076SAndreas Gohrclass action_plugin_twofactor_login extends DokuWiki_Action_Plugin 35fca58076SAndreas Gohr{ 36848a9be0SAndreas Gohr const TWOFACTOR_COOKIE = '2FA' . DOKU_COOKIE; 37848a9be0SAndreas Gohr 38a386a536SAndreas Gohr /** @var Manager */ 39a386a536SAndreas Gohr protected $manager; 40fca58076SAndreas Gohr 41a386a536SAndreas Gohr /** 42a386a536SAndreas Gohr * Constructor 43a386a536SAndreas Gohr */ 44fca58076SAndreas Gohr public function __construct() 45fca58076SAndreas Gohr { 46a386a536SAndreas Gohr $this->manager = Manager::getInstance(); 47fca58076SAndreas Gohr } 48fca58076SAndreas Gohr 49fca58076SAndreas Gohr /** 50fca58076SAndreas Gohr * Registers the event handlers. 51fca58076SAndreas Gohr */ 52fca58076SAndreas Gohr public function register(Doku_Event_Handler $controller) 53fca58076SAndreas Gohr { 548b7620a8SAndreas Gohr if (!(Manager::getInstance())->isReady()) return; 558b7620a8SAndreas Gohr 56a386a536SAndreas Gohr // check 2fa requirements and either move to profile or login handling 57a386a536SAndreas Gohr $controller->register_hook( 58a386a536SAndreas Gohr 'ACTION_ACT_PREPROCESS', 59a386a536SAndreas Gohr 'BEFORE', 60a386a536SAndreas Gohr $this, 61a386a536SAndreas Gohr 'handleActionPreProcess', 62a386a536SAndreas Gohr null, 63a386a536SAndreas Gohr -999999 64a386a536SAndreas Gohr ); 65fca58076SAndreas Gohr 66a386a536SAndreas Gohr // display login form 67a386a536SAndreas Gohr $controller->register_hook( 68a386a536SAndreas Gohr 'TPL_ACT_UNKNOWN', 69a386a536SAndreas Gohr 'BEFORE', 70a386a536SAndreas Gohr $this, 71a386a536SAndreas Gohr 'handleLoginDisplay' 72a386a536SAndreas Gohr ); 73a386a536SAndreas Gohr 74a386a536SAndreas Gohr // FIXME disable user in all non-main screens (media, detail, ajax, ...) 75a386a536SAndreas Gohr 76a386a536SAndreas Gohr /* 77fca58076SAndreas Gohr $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'twofactor_login_form'); 78fca58076SAndreas Gohr 79fca58076SAndreas Gohr // Manage action flow around the twofactor authentication requirements. 80a386a536SAndreas Gohr 81fca58076SAndreas Gohr // Handle the twofactor login and profile actions. 82fca58076SAndreas Gohr $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'twofactor_handle_unknown_action'); 83fca58076SAndreas Gohr $controller->register_hook('TPL_ACTION_GET', 'BEFORE', $this, 'twofactor_get_unknown_action'); 84fca58076SAndreas Gohr 85fca58076SAndreas Gohr // If the user supplies a token code at login, checks it before logging the user in. 86fca58076SAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'twofactor_before_auth_check', null, 87fca58076SAndreas Gohr -999999); 88fca58076SAndreas Gohr // Atempts to process the second login if the user hasn't done so already. 89fca58076SAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'AFTER', $this, 'twofactor_after_auth_check'); 90fca58076SAndreas Gohr $this->log('register: Session: ' . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS); 91a386a536SAndreas Gohr */ 92fca58076SAndreas Gohr } 93fca58076SAndreas Gohr 94fca58076SAndreas Gohr /** 95a386a536SAndreas Gohr * Decide if any 2fa handling needs to be done for the current user 96a386a536SAndreas Gohr * 97a386a536SAndreas Gohr * @param Doku_Event $event 98a386a536SAndreas Gohr */ 99a386a536SAndreas Gohr public function handleActionPreProcess(Doku_Event $event) 100a386a536SAndreas Gohr { 101a386a536SAndreas Gohr if (!$this->manager->getUser()) return; 102a386a536SAndreas Gohr 103a386a536SAndreas Gohr global $INPUT; 104a386a536SAndreas Gohr 105a386a536SAndreas Gohr // already in a 2fa login? 106a386a536SAndreas Gohr if ($event->data === 'twofactor_login') { 107848a9be0SAndreas Gohr if ($this->verify( 108848a9be0SAndreas Gohr $INPUT->str('2fa_code'), 109848a9be0SAndreas Gohr $INPUT->str('2fa_provider'), 110848a9be0SAndreas Gohr $INPUT->bool('sticky') 111848a9be0SAndreas Gohr )) { 112a386a536SAndreas Gohr $event->data = 'show'; 113848a9be0SAndreas Gohr return; 114a386a536SAndreas Gohr } else { 115a386a536SAndreas Gohr // show form 116a386a536SAndreas Gohr $event->preventDefault(); 117a386a536SAndreas Gohr return; 118a386a536SAndreas Gohr } 119a386a536SAndreas Gohr } 120a386a536SAndreas Gohr 121a386a536SAndreas Gohr // authed already, continue 122a386a536SAndreas Gohr if ($this->isAuthed()) { 123a386a536SAndreas Gohr return; 124a386a536SAndreas Gohr } 125a386a536SAndreas Gohr 126a386a536SAndreas Gohr if (count($this->manager->getUserProviders())) { 127a386a536SAndreas Gohr // user has already 2fa set up - they need to authenticate before anything else 128a386a536SAndreas Gohr $event->data = 'twofactor_login'; 129a386a536SAndreas Gohr $event->preventDefault(); 130a386a536SAndreas Gohr $event->stopPropagation(); 131a386a536SAndreas Gohr return; 132a386a536SAndreas Gohr } 133a386a536SAndreas Gohr 134a386a536SAndreas Gohr if ($this->manager->isRequired()) { 135a386a536SAndreas Gohr // 2fa is required - they need to set it up now 136a386a536SAndreas Gohr // this will be handled by action/profile.php 137a386a536SAndreas Gohr $event->data = 'twofactor_profile'; 138a386a536SAndreas Gohr } 139a386a536SAndreas Gohr 140a386a536SAndreas Gohr // all good. proceed 141a386a536SAndreas Gohr } 142a386a536SAndreas Gohr 143a386a536SAndreas Gohr /** 144a386a536SAndreas Gohr * Show a 2fa login screen 145a386a536SAndreas Gohr * 146a386a536SAndreas Gohr * @param Doku_Event $event 147a386a536SAndreas Gohr */ 148a386a536SAndreas Gohr public function handleLoginDisplay(Doku_Event $event) 149a386a536SAndreas Gohr { 150a386a536SAndreas Gohr if ($event->data !== 'twofactor_login') return; 151a386a536SAndreas Gohr $event->preventDefault(); 152a386a536SAndreas Gohr $event->stopPropagation(); 153a386a536SAndreas Gohr 154a386a536SAndreas Gohr global $INPUT; 155a386a536SAndreas Gohr $providerID = $INPUT->str('2fa_provider'); 156a386a536SAndreas Gohr $providers = $this->manager->getUserProviders(); 157a386a536SAndreas Gohr if (isset($providers[$providerID])) { 158a386a536SAndreas Gohr $provider = $providers[$providerID]; 159a386a536SAndreas Gohr } else { 160*b6119621SAndreas Gohr $provider = $this->manager->getUserDefaultProvider(); 161a386a536SAndreas Gohr } 162*b6119621SAndreas Gohr // remove current provider from list 163*b6119621SAndreas Gohr unset($providers[$provider->getProviderID()]); 164a386a536SAndreas Gohr 165848a9be0SAndreas Gohr $form = new dokuwiki\Form\Form(['method' => 'POST']); 166848a9be0SAndreas Gohr $form->setHiddenField('do', 'twofactor_login'); 167a386a536SAndreas Gohr $form->setHiddenField('2fa_provider', $provider->getProviderID()); 168a386a536SAndreas Gohr $form->addFieldsetOpen($provider->getLabel()); 169a386a536SAndreas Gohr try { 170a386a536SAndreas Gohr $code = $provider->generateCode(); 171a386a536SAndreas Gohr $info = $provider->transmitMessage($code); 172a386a536SAndreas Gohr $form->addHTML('<p>' . hsc($info) . '</p>'); 173848a9be0SAndreas Gohr $form->addTextInput('2fa_code', 'Your Code')->val(''); 174848a9be0SAndreas Gohr $form->addCheckbox('sticky', 'Remember this browser'); // reuse same name as login 175a386a536SAndreas Gohr $form->addButton('2fa', 'Submit')->attr('type', 'submit'); 176a386a536SAndreas Gohr } catch (\Exception $e) { 177a386a536SAndreas Gohr msg(hsc($e->getMessage()), -1); // FIXME better handling 178a386a536SAndreas Gohr } 179a386a536SAndreas Gohr $form->addFieldsetClose(); 180a386a536SAndreas Gohr 181a386a536SAndreas Gohr if (count($providers)) { 182a386a536SAndreas Gohr $form->addFieldsetOpen('Alternative methods'); 183a386a536SAndreas Gohr foreach ($providers as $prov) { 184a386a536SAndreas Gohr $link = $prov->getProviderID(); // FIXME build correct links 185a386a536SAndreas Gohr 186a386a536SAndreas Gohr $form->addHTML($link); 187a386a536SAndreas Gohr } 188a386a536SAndreas Gohr $form->addFieldsetClose(); 189a386a536SAndreas Gohr } 190a386a536SAndreas Gohr 191a386a536SAndreas Gohr echo $form->toHTML(); 192a386a536SAndreas Gohr } 193a386a536SAndreas Gohr 194a386a536SAndreas Gohr /** 195a386a536SAndreas Gohr * Has the user already authenticated with the second factor? 196a386a536SAndreas Gohr * @return bool 197a386a536SAndreas Gohr */ 198a386a536SAndreas Gohr protected function isAuthed() 199a386a536SAndreas Gohr { 200848a9be0SAndreas Gohr if (!isset($_COOKIE[self::TWOFACTOR_COOKIE])) return false; 201848a9be0SAndreas Gohr $data = unserialize(base64_decode($_COOKIE[self::TWOFACTOR_COOKIE])); 202848a9be0SAndreas Gohr if (!is_array($data)) return false; 203848a9be0SAndreas Gohr list($providerID, $buid,) = $data; 204848a9be0SAndreas Gohr if (auth_browseruid() !== $buid) return false; 205848a9be0SAndreas Gohr 206848a9be0SAndreas Gohr try { 207848a9be0SAndreas Gohr // ensure it's a still valid provider 208848a9be0SAndreas Gohr $this->manager->getUserProvider($providerID); 209848a9be0SAndreas Gohr return true; 210848a9be0SAndreas Gohr } catch (\Exception $e) { 211a386a536SAndreas Gohr return false; 212a386a536SAndreas Gohr } 213848a9be0SAndreas Gohr } 214a386a536SAndreas Gohr 215a386a536SAndreas Gohr /** 216a386a536SAndreas Gohr * Verify a given code 217a386a536SAndreas Gohr * 218a386a536SAndreas Gohr * @return bool 219a386a536SAndreas Gohr * @throws Exception 220a386a536SAndreas Gohr */ 221848a9be0SAndreas Gohr protected function verify($code, $providerID, $sticky) 222a386a536SAndreas Gohr { 223848a9be0SAndreas Gohr global $conf; 224848a9be0SAndreas Gohr 225a386a536SAndreas Gohr if (!$code) return false; 226a386a536SAndreas Gohr if (!$providerID) return false; 227a386a536SAndreas Gohr $provider = $this->manager->getUserProvider($providerID); 228a386a536SAndreas Gohr $ok = $provider->checkCode($code); 229848a9be0SAndreas Gohr if (!$ok) { 230848a9be0SAndreas Gohr msg('code was wrong', -1); 231848a9be0SAndreas Gohr return false; 232848a9be0SAndreas Gohr } 233a386a536SAndreas Gohr 234848a9be0SAndreas Gohr // store cookie 235848a9be0SAndreas Gohr $data = base64_encode(serialize([$providerID, auth_browseruid(), time()])); 236848a9be0SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 237848a9be0SAndreas Gohr $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year 238848a9be0SAndreas Gohr setcookie(self::TWOFACTOR_COOKIE, $data, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 239a386a536SAndreas Gohr 240a386a536SAndreas Gohr return true; 241a386a536SAndreas Gohr } 242a386a536SAndreas Gohr 243a386a536SAndreas Gohr 244a386a536SAndreas Gohr // region old shit 245a386a536SAndreas Gohr 246a386a536SAndreas Gohr /** 247fca58076SAndreas Gohr * Handles the login form rendering. 248fca58076SAndreas Gohr */ 249fca58076SAndreas Gohr public function twofactor_login_form(&$event, $param) 250fca58076SAndreas Gohr { 251fca58076SAndreas Gohr $this->log('twofactor_login_form: start', self::LOGGING_DEBUG); 252fca58076SAndreas Gohr $twofa_form = form_makeTextField('otp', '', $this->getLang('twofactor_login'), '', 'block', 253fca58076SAndreas Gohr array('size' => '50', 'autocomplete' => 'off')); 254fca58076SAndreas Gohr $pos = $event->data->findElementByAttribute('name', 'p'); 255fca58076SAndreas Gohr $event->data->insertElement($pos + 1, $twofa_form); 256fca58076SAndreas Gohr } 257fca58076SAndreas Gohr 258fca58076SAndreas Gohr /** 259fca58076SAndreas Gohr * Action process redirector. If logging out, processes the logout 260fca58076SAndreas Gohr * function. If visiting the profile, sets a flag to confirm that the 261fca58076SAndreas Gohr * profile is being viewed in order to enable OTP attribute updates. 262fca58076SAndreas Gohr */ 263fca58076SAndreas Gohr public function twofactor_action_process_handler(&$event, $param) 264fca58076SAndreas Gohr { 265fca58076SAndreas Gohr global $USERINFO, $ID, $INFO, $INPUT; 266fca58076SAndreas Gohr $this->log('twofactor_action_process_handler: start ' . $event->data, self::LOGGING_DEBUG); 267fca58076SAndreas Gohr // Handle logout. 268fca58076SAndreas Gohr if ($event->data == 'logout') { 269fca58076SAndreas Gohr $this->_logout(); 270fca58076SAndreas Gohr return; 271fca58076SAndreas Gohr } 272fca58076SAndreas Gohr // Handle main login. 273fca58076SAndreas Gohr if ($event->data == 'login') { 274fca58076SAndreas Gohr // To support loglog or any other module that hooks login checking for success, 275fca58076SAndreas Gohr // Confirm that the user is logged in. If not, then redirect to twofactor_login 276fca58076SAndreas Gohr // and fail the login. 277fca58076SAndreas Gohr if ($USERINFO && !$this->get_clearance()) { 278fca58076SAndreas Gohr // Hijack this event. We need to resend it after 2FA is done. 279fca58076SAndreas Gohr $event->stopPropagation(); 280fca58076SAndreas Gohr // Send loglog an event to show the user logged in but needs OTP code. 281fca58076SAndreas Gohr $log = array('message' => 'logged in, ' . $this->getLang('requires_otp'), 'user' => $user); 282fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 283fca58076SAndreas Gohr } 284fca58076SAndreas Gohr return; 285fca58076SAndreas Gohr } 286fca58076SAndreas Gohr 287fca58076SAndreas Gohr // Check to see if we are heading to the twofactor login. 288fca58076SAndreas Gohr if ($event->data == 'twofactor_login') { 289fca58076SAndreas Gohr // Check if we already have clearance- just in case. 290fca58076SAndreas Gohr if ($this->get_clearance()) { 291fca58076SAndreas Gohr // Okay, this continues on with normal processing. 292fca58076SAndreas Gohr return; 293fca58076SAndreas Gohr } 294fca58076SAndreas Gohr // We will be handling this action's permissions here. 295fca58076SAndreas Gohr $event->preventDefault(); 296fca58076SAndreas Gohr $event->stopPropagation(); 297fca58076SAndreas Gohr // If not logged into the main auth plugin then send there. 298fca58076SAndreas Gohr if (!$USERINFO) { 299fca58076SAndreas Gohr $event->result = false; 300fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'login'), true, '&')); 301fca58076SAndreas Gohr return; 302fca58076SAndreas Gohr } 303fca58076SAndreas Gohr if (count($this->otpMods) == 0) { 304fca58076SAndreas Gohr $this->log('No available otp modules.', self::LOGGING_DEBUG); 305fca58076SAndreas Gohr // There is no way to handle this login. 306fca58076SAndreas Gohr msg($this->getLang('mustusetoken'), -1); 307fca58076SAndreas Gohr $event->result = false; 308fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'logout'), true, '&')); 309fca58076SAndreas Gohr return; 310fca58076SAndreas Gohr } 311fca58076SAndreas Gohr // Otherwise handle the action. 312fca58076SAndreas Gohr $act = $this->_process_otp($event, $param); 313fca58076SAndreas Gohr $event->result = true; 314fca58076SAndreas Gohr if ($act) { 315fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => $act), true, '&')); 316fca58076SAndreas Gohr } 317fca58076SAndreas Gohr return; 318fca58076SAndreas Gohr } 319fca58076SAndreas Gohr 320fca58076SAndreas Gohr // Is the user logged into the wiki? 321fca58076SAndreas Gohr if (!$USERINFO) { 322fca58076SAndreas Gohr // If not logged in, then do nothing. 323fca58076SAndreas Gohr return; 324fca58076SAndreas Gohr } 325fca58076SAndreas Gohr 326fca58076SAndreas Gohr // See if this user has any OTP methods configured. 327fca58076SAndreas Gohr $available = count($this->tokenMods) + count($this->otpMods) > 0; 328fca58076SAndreas Gohr // Check if this user needs to login with 2FA. 329fca58076SAndreas Gohr // Wiki mandatory is on if user is logged in and config is mandatory 330fca58076SAndreas Gohr $mandatory = $this->getConf("optinout") == 'mandatory' && $INPUT->server->str('REMOTE_USER', ''); 331fca58076SAndreas Gohr // User is NOT OPTED OUT if the optin setting is undefined and the wiki config is optout. 332fca58076SAndreas Gohr $not_opted_out = $this->attribute->get("twofactor", "state") == '' && $this->getConf("optinout") == 'optout'; 333fca58076SAndreas Gohr // The user must login if wiki mandatory is on or if the user is logged in and user is opt in. 334fca58076SAndreas Gohr $must_login = $mandatory || ($this->attribute->get("twofactor", 335fca58076SAndreas Gohr "state") == 'in' && $INPUT->server->str('REMOTE_USER', '')); 336fca58076SAndreas Gohr $has_clearance = $this->get_clearance() === true; 337fca58076SAndreas Gohr $this->log('twofactor_action_process_handler: USERINFO: ' . print_r($USERINFO, true), self::LOGGING_DEBUGPLUS); 338fca58076SAndreas Gohr 339fca58076SAndreas Gohr // Possible combination skipped- not logged in and 2FA is not requred for user {optout conf or (no selection and optin conf)}. 340fca58076SAndreas Gohr 341fca58076SAndreas Gohr // Check to see if updating twofactor is required. 342fca58076SAndreas Gohr // This happens if the wiki is mandatory, the user has not opted out of an opt-out wiki, or if the user has opted in, and if there are no available mods for use. 343fca58076SAndreas Gohr // The user cannot have available mods without setting them up, and cannot unless the wiki is mandatory or the user has opted in. 344fca58076SAndreas Gohr if (($must_login || $not_opted_out) && !$available) { 345fca58076SAndreas Gohr // If the user has not been granted access at this point, do so or they will get booted after setting up 2FA. 346fca58076SAndreas Gohr if (!$has_clearance) { 347fca58076SAndreas Gohr $this->_grant_clearance(); 348fca58076SAndreas Gohr } 349fca58076SAndreas Gohr // We need to go to the twofactor profile. 350fca58076SAndreas Gohr // If we were setup properly, we would not be here in the code. 351fca58076SAndreas Gohr $event->preventDefault(); 352fca58076SAndreas Gohr $event->stopPropagation(); 353fca58076SAndreas Gohr $event->result = false; 354fca58076SAndreas Gohr // Send loglog an event to show the user aborted 2FA. 355fca58076SAndreas Gohr $log = array('message' => 'logged in, ' . $this->getLang('2fa_mandatory'), 'user' => $user); 356fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 357fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'twofactor_profile'), true, '&')); 358fca58076SAndreas Gohr return; 359fca58076SAndreas Gohr } 360fca58076SAndreas Gohr 361fca58076SAndreas Gohr // Now validate login before proceeding. 362fca58076SAndreas Gohr if (!$has_clearance) { 363fca58076SAndreas Gohr if ($must_login) { 364fca58076SAndreas Gohr if (!in_array($event->data, array('login', 'twofactor_login'))) { 365fca58076SAndreas Gohr // If not logged in then force to the login page. 366fca58076SAndreas Gohr $event->preventDefault(); 367fca58076SAndreas Gohr $event->stopPropagation(); 368fca58076SAndreas Gohr $event->result = false; 369fca58076SAndreas Gohr // If there are OTP generators, then use them. 370fca58076SAndreas Gohr send_redirect(wl($ID, array('do' => 'twofactor_login'), true, '&')); 371fca58076SAndreas Gohr return; 372fca58076SAndreas Gohr } 373fca58076SAndreas Gohr // Otherwise go to where we are told. 374fca58076SAndreas Gohr return; 375fca58076SAndreas Gohr } 376fca58076SAndreas Gohr // The user is not set with 2FA and is not required to. 377fca58076SAndreas Gohr // Grant clearance and continue. 378fca58076SAndreas Gohr $this->_grant_clearance(); 379fca58076SAndreas Gohr } 380fca58076SAndreas Gohr // Otherwise everything is good! 381fca58076SAndreas Gohr return; 382fca58076SAndreas Gohr } 383fca58076SAndreas Gohr 384fca58076SAndreas Gohr public function twofactor_handle_unknown_action(Doku_Event $event, $param) 385fca58076SAndreas Gohr { 386fca58076SAndreas Gohr if ($event->data == 'twofactor_login') { 387fca58076SAndreas Gohr $event->preventDefault(); 388fca58076SAndreas Gohr $event->stopPropagation(); 389fca58076SAndreas Gohr $event->result = $this->twofactor_otp_login($event, $param); 390fca58076SAndreas Gohr return; 391fca58076SAndreas Gohr } 392fca58076SAndreas Gohr } 393fca58076SAndreas Gohr 394fca58076SAndreas Gohr public function twofactor_get_unknown_action(&$event, $param) 395fca58076SAndreas Gohr { 396fca58076SAndreas Gohr $this->log('start: twofactor_before_auth_check', self::LOGGING_DEBUG); 397fca58076SAndreas Gohr switch ($event->data['type']) { 398fca58076SAndreas Gohr case 'twofactor_profile': 399fca58076SAndreas Gohr $event->data['params'] = array('do' => 'twofactor_profile'); 400fca58076SAndreas Gohr // Inject text into $lang. 401fca58076SAndreas Gohr global $lang; 402fca58076SAndreas Gohr $lang['btn_twofactor_profile'] = $this->getLang('btn_twofactor_profile'); 403fca58076SAndreas Gohr $event->preventDefault(); 404fca58076SAndreas Gohr $event->stopPropagation(); 405fca58076SAndreas Gohr $event->result = false; 406fca58076SAndreas Gohr break; 407fca58076SAndreas Gohr } 408fca58076SAndreas Gohr } 409fca58076SAndreas Gohr 410fca58076SAndreas Gohr /** 411fca58076SAndreas Gohr * Logout this session from two factor authentication. Purge any existing 412fca58076SAndreas Gohr * OTP from the user's attributes. 413fca58076SAndreas Gohr */ 414fca58076SAndreas Gohr private function _logout() 415fca58076SAndreas Gohr { 416fca58076SAndreas Gohr global $conf, $INPUT; 417fca58076SAndreas Gohr $this->log('_logout: start', self::LOGGING_DEBUG); 418fca58076SAndreas Gohr $this->log(print_r(array($_SESSION, $_COOKIE), true), self::LOGGING_DEBUGPLUS); 419fca58076SAndreas Gohr // No need to do this as long as no Cookie or session for login is present! 420fca58076SAndreas Gohr if (empty($_SESSION[DOKU_COOKIE]['twofactor_clearance']) && empty($_COOKIE[TWOFACTOR_COOKIE])) { 421fca58076SAndreas Gohr $this->log('_logout: quitting, no cookies', self::LOGGING_DEBUG); 422fca58076SAndreas Gohr return; 423fca58076SAndreas Gohr } 424fca58076SAndreas Gohr // Audit log. 425fca58076SAndreas Gohr $this->log("2FA Logout: " . $INPUT->server->str('REMOTE_USER', $_REQUEST['r']), self::LOGGING_AUDIT); 426fca58076SAndreas Gohr if ($this->attribute) { 427fca58076SAndreas Gohr // Purge outstanding OTPs. 428fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp"); 429fca58076SAndreas Gohr // Purge cookie and session ID relation. 430fca58076SAndreas Gohr $key = $_COOKIE[TWOFACTOR_COOKIE]; 431fca58076SAndreas Gohr if (!empty($key) && substr($key, 0, 3) != 'id.') { 432fca58076SAndreas Gohr $id = $this->attribute->del("twofactor", $key); 433fca58076SAndreas Gohr } 434fca58076SAndreas Gohr // Wipe out 2FA cookie. 435fca58076SAndreas Gohr $this->log('del cookies: ' . TWOFACTOR_COOKIE . ' ' . print_r(headers_sent(), true), 436fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 437fca58076SAndreas Gohr $cookie = ''; 438fca58076SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 439fca58076SAndreas Gohr $time = time() - 600000; //many seconds ago 440fca58076SAndreas Gohr setcookie(TWOFACTOR_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 441fca58076SAndreas Gohr unset($_COOKIE[TWOFACTOR_COOKIE]); 442fca58076SAndreas Gohr // Just in case, unset the setTime flag so attributes will be saved again. 443fca58076SAndreas Gohr $this->setTime = false; 444fca58076SAndreas Gohr } 445fca58076SAndreas Gohr // Before we get here, the session is closed. Reopen it to logout the user. 446fca58076SAndreas Gohr if (!headers_sent()) { 447fca58076SAndreas Gohr $session = session_status() != PHP_SESSION_NONE; 448fca58076SAndreas Gohr if (!$session) { 449fca58076SAndreas Gohr session_start(); 450fca58076SAndreas Gohr } 451fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = false; 452fca58076SAndreas Gohr unset($_SESSION[DOKU_COOKIE]['twofactor_clearance']); 453fca58076SAndreas Gohr if (!$session) { 454fca58076SAndreas Gohr session_write_close(); 455fca58076SAndreas Gohr } 456fca58076SAndreas Gohr } else { 457fca58076SAndreas Gohr msg("Error! You have not been logged off!!!", -1); 458fca58076SAndreas Gohr } 459fca58076SAndreas Gohr } 460fca58076SAndreas Gohr 461fca58076SAndreas Gohr /** 462fca58076SAndreas Gohr * See if the current session has passed two factor authentication. 463fca58076SAndreas Gohr * @return bool - true if the session as successfully passed two factor 464fca58076SAndreas Gohr * authentication. 465fca58076SAndreas Gohr */ 466fca58076SAndreas Gohr public function get_clearance($user = null) 467fca58076SAndreas Gohr { 468fca58076SAndreas Gohr global $INPUT; 469fca58076SAndreas Gohr $this->log("get_clearance: start", self::LOGGING_DEBUG); 470fca58076SAndreas Gohr $this->log("User:" . $INPUT->server->str('REMOTE_USER', null), self::LOGGING_DEBUGPLUS); 471fca58076SAndreas Gohr # Get and correct the refresh expiry. 472fca58076SAndreas Gohr # At least 5 min, at most 1440 min (1 day). 473fca58076SAndreas Gohr $refreshexpiry = min(max($this->getConf('refreshexpiry'), 5), 1400) * 60; 474fca58076SAndreas Gohr # First check if we have a key. No key === no login. 475fca58076SAndreas Gohr $key = $_COOKIE[TWOFACTOR_COOKIE]; 476fca58076SAndreas Gohr if (empty($key)) { 477fca58076SAndreas Gohr $this->log("get_clearance: No cookie.", self::LOGGING_DEBUGPLUS); 478fca58076SAndreas Gohr return false; 479fca58076SAndreas Gohr } 480fca58076SAndreas Gohr # If the key is not valid, logout. 481fca58076SAndreas Gohr if (substr($key, 0, 3) != 'id.') { 482fca58076SAndreas Gohr $this->log("get_clearance: BAD cookie.", self::LOGGING_DEBUGPLUS); 483fca58076SAndreas Gohr // Purge the login data just in case. 484fca58076SAndreas Gohr $this->_logout(); 485fca58076SAndreas Gohr return false; 486fca58076SAndreas Gohr } 487fca58076SAndreas Gohr # Load the expiry value from session. 488fca58076SAndreas Gohr $expiry = $_SESSION[DOKU_COOKIE]['twofactor_clearance']; 489fca58076SAndreas Gohr # Check if this time is valid. 490fca58076SAndreas Gohr $clearance = (!empty($expiry) && $expiry + $refreshexpiry > time()); 491fca58076SAndreas Gohr if (!$clearance) { 492fca58076SAndreas Gohr # First use this time to purge the old IDs from attribute. 493fca58076SAndreas Gohr foreach (array_filter($this->attribute->enumerateAttributes("twofactor", $user), function ($key) { 494fca58076SAndreas Gohr substr($key, 0, 3) == 'id.'; 495fca58076SAndreas Gohr }) as $attr) { 496fca58076SAndreas Gohr if ($this->attribute->get("twofactor", $attr, $user) + $refreshexpiry < time()) { 497fca58076SAndreas Gohr $this->attribute->del("twofactor", $attr, $user); 498fca58076SAndreas Gohr } 499fca58076SAndreas Gohr } 500fca58076SAndreas Gohr # Check if this key still exists. 501fca58076SAndreas Gohr $clearance = $this->attribute->exists("twofactor", $key, $user); 502fca58076SAndreas Gohr if ($clearance) { 503fca58076SAndreas Gohr $this->log("get_clearance: 2FA revived by cookie. Expiry: " . print_r($expiry, 504fca58076SAndreas Gohr true) . " Session: " . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS); 505fca58076SAndreas Gohr } 506fca58076SAndreas Gohr } 507fca58076SAndreas Gohr if ($clearance && !$this->setTime) { 508fca58076SAndreas Gohr $session = session_status() != PHP_SESSION_NONE; 509fca58076SAndreas Gohr if (!$session) { 510fca58076SAndreas Gohr session_start(); 511fca58076SAndreas Gohr } 512fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = time(); 513fca58076SAndreas Gohr if (!$session) { 514fca58076SAndreas Gohr session_write_close(); 515fca58076SAndreas Gohr } 516fca58076SAndreas Gohr $this->attribute->set("twofactor", $key, $_SESSION[DOKU_COOKIE]['twofactor_clearance'], $user); 517fca58076SAndreas Gohr // Set this flag to stop future updates. 518fca58076SAndreas Gohr $this->setTime = true; 519fca58076SAndreas Gohr $this->log("get_clearance: Session reset. Session: " . print_r($_SESSION, true), self::LOGGING_DEBUGPLUS); 520fca58076SAndreas Gohr } elseif (!$clearance) { 521fca58076SAndreas Gohr // Otherwise logout. 522fca58076SAndreas Gohr $this->_logout(); 523fca58076SAndreas Gohr } 524fca58076SAndreas Gohr return $clearance; 525fca58076SAndreas Gohr } 526fca58076SAndreas Gohr 527fca58076SAndreas Gohr /** 528fca58076SAndreas Gohr * Flags this session as having passed two factor authentication. 529fca58076SAndreas Gohr * @return bool - returns true on successfully granting two factor clearance. 530fca58076SAndreas Gohr */ 531fca58076SAndreas Gohr private function _grant_clearance($user = null) 532fca58076SAndreas Gohr { 533fca58076SAndreas Gohr global $conf, $INPUT; 534fca58076SAndreas Gohr $this->log("_grant_clearance: start", self::LOGGING_DEBUG); 535fca58076SAndreas Gohr $this->log('2FA Login: ' . $INPUT->server->str("REMOTE_USER", $user), self::LOGGING_AUDIT); 536fca58076SAndreas Gohr if ($INPUT->server->str("REMOTE_USER", $user) == 1) { 537fca58076SAndreas Gohr $this->log("_grant_clearance: start", self::LOGGING_DEBUGPLUS); 538fca58076SAndreas Gohr } 539fca58076SAndreas Gohr // Purge the otp code as a security measure. 540fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp", $user); 541fca58076SAndreas Gohr if (!headers_sent()) { 542fca58076SAndreas Gohr $session = session_status() != PHP_SESSION_NONE; 543fca58076SAndreas Gohr if (!$session) { 544fca58076SAndreas Gohr session_start(); 545fca58076SAndreas Gohr } 546fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_clearance'] = time(); 547fca58076SAndreas Gohr // Set the notify flag if set or required by wiki. 548fca58076SAndreas Gohr $this->log('_grant_clearance: conf:' . $this->getConf('loginnotice') . ' user:' . ($this->attribute->get("twofactor", 549fca58076SAndreas Gohr "loginnotice", $user) === true ? 'true' : 'false'), self::LOGGING_DEBUG); 550fca58076SAndreas Gohr $send_wanted = $this->getConf('loginnotice') == 'always' || ($this->getConf('loginnotice') == 'user' && $this->attribute->get("twofactor", 551fca58076SAndreas Gohr "loginnotice", $user) == true); 552fca58076SAndreas Gohr if ($send_wanted) { 553fca58076SAndreas Gohr $_SESSION[DOKU_COOKIE]['twofactor_notify'] = true; 554fca58076SAndreas Gohr } 555fca58076SAndreas Gohr if (!$session) { 556fca58076SAndreas Gohr session_write_close(); 557fca58076SAndreas Gohr } 558fca58076SAndreas Gohr } else { 559fca58076SAndreas Gohr msg("Error! You have not been logged in!!!", -1); 560fca58076SAndreas Gohr } 561fca58076SAndreas Gohr // Creating a cookie in case the session purges. 562fca58076SAndreas Gohr $key = 'id.' . session_id(); 563fca58076SAndreas Gohr // Storing a timeout value. 564fca58076SAndreas Gohr $this->attribute->set("twofactor", $key, $_SESSION[DOKU_COOKIE]['twofactor_clearance'], $user); 565fca58076SAndreas Gohr // Set the 2FA cookie. 566fca58076SAndreas Gohr $this->log('_grant_clearance: new cookies: ' . TWOFACTOR_COOKIE . ' ' . print_r(headers_sent(), true), 567fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 568fca58076SAndreas Gohr $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 569fca58076SAndreas Gohr $time = time() + 60 * 60 * 24 * 365; //one year 570fca58076SAndreas Gohr setcookie(TWOFACTOR_COOKIE, $key, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true); 571fca58076SAndreas Gohr $_COOKIE[TWOFACTOR_COOKIE] = $key; 572fca58076SAndreas Gohr return !empty($_SESSION[DOKU_COOKIE]['twofactor_clearance']); 573fca58076SAndreas Gohr } 574fca58076SAndreas Gohr 575fca58076SAndreas Gohr /** 576fca58076SAndreas Gohr * Sends emails notifying user of successfult 2FA login. 577fca58076SAndreas Gohr * @return mixed - returns true on successfully sending notification to all 578fca58076SAndreas Gohr * modules, false if no notifications were sent, or a number indicating 579fca58076SAndreas Gohr * the number of modules that suceeded. 580fca58076SAndreas Gohr */ 581fca58076SAndreas Gohr private function _send_login_notification() 582fca58076SAndreas Gohr { 583fca58076SAndreas Gohr $this->log("_send_login_notification: start", self::LOGGING_DEBUG); 584fca58076SAndreas Gohr // Send login notification. 585fca58076SAndreas Gohr $module = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor", 586fca58076SAndreas Gohr "defaultmod") : null; 587fca58076SAndreas Gohr $subject = $this->getConf('loginsubject'); 588fca58076SAndreas Gohr $time = date(DATE_RFC2822); 589fca58076SAndreas Gohr $message = str_replace('$time', $time, $this->getConf('logincontent')); 590fca58076SAndreas Gohr $result = $this->_send_message($subject, $message, $module); 591fca58076SAndreas Gohr return $result; 592fca58076SAndreas Gohr } 593fca58076SAndreas Gohr 594fca58076SAndreas Gohr /** 595fca58076SAndreas Gohr * Handles the authentication check. Screens Google Authenticator OTP, if available. 596fca58076SAndreas Gohr * NOTE: NOT LOGGED IN YET. Attribute requires user name. 597fca58076SAndreas Gohr */ 598fca58076SAndreas Gohr function twofactor_before_auth_check(&$event, $param) 599fca58076SAndreas Gohr { 600fca58076SAndreas Gohr global $ACT, $INPUT; 601fca58076SAndreas Gohr $this->log("twofactor_before_auth_check: start $ACT", self::LOGGING_DEBUG); 602fca58076SAndreas Gohr $this->log("twofactor_before_auth_check: Cookie: " . print_r($_COOKIE, true), self::LOGGING_DEBUGPLUS); 603fca58076SAndreas Gohr // Only operate if this is a login. 604fca58076SAndreas Gohr if ($ACT !== 'login') { 605fca58076SAndreas Gohr return; 606fca58076SAndreas Gohr } 607fca58076SAndreas Gohr // If there is no supplied username, then there is nothing to check at this time. 608fca58076SAndreas Gohr if (!$event->data['user']) { 609fca58076SAndreas Gohr return; 610fca58076SAndreas Gohr } 611fca58076SAndreas Gohr $user = $INPUT->server->str('REMOTE_USER', $event->data['user']); 612fca58076SAndreas Gohr // Set helper variables here. 613fca58076SAndreas Gohr $this->_setHelperVariables($user); 614fca58076SAndreas Gohr // If the user still has clearance, then we can skip this. 615fca58076SAndreas Gohr if ($this->get_clearance($user)) { 616fca58076SAndreas Gohr return; 617fca58076SAndreas Gohr } 618fca58076SAndreas Gohr // Allow the user to try to use login tokens, even if the account cannot use them. 619fca58076SAndreas Gohr $otp = $INPUT->str('otp', ''); 620fca58076SAndreas Gohr if ($otp !== '') { 621fca58076SAndreas Gohr // Check for any modules that support OTP at login and are ready for use. 622fca58076SAndreas Gohr foreach ($this->tokenMods as $mod) { 623fca58076SAndreas Gohr $result = $mod->processLogin($otp, $user); 624fca58076SAndreas Gohr if ($result) { 625fca58076SAndreas Gohr // The OTP code was valid. 626fca58076SAndreas Gohr $this->_grant_clearance($user); 627fca58076SAndreas Gohr // Send loglog an event to show the user logged in using a token. 628fca58076SAndreas Gohr $log = array('message' => 'logged in ' . $this->getLang('token_ok'), 'user' => $user); 629fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 630fca58076SAndreas Gohr return; 631fca58076SAndreas Gohr } 632fca58076SAndreas Gohr } 633fca58076SAndreas Gohr global $lang; 634fca58076SAndreas Gohr msg($lang['badlogin'], -1); 635fca58076SAndreas Gohr $event->preventDefault(); 636fca58076SAndreas Gohr $event->result = false; 637fca58076SAndreas Gohr // Send loglog an event to show the failure 638fca58076SAndreas Gohr if (count($this->tokenMods) == 0) { 639fca58076SAndreas Gohr $log = array('message' => 'failed ' . $this->getLang('no_tokens'), 'user' => $user); 640fca58076SAndreas Gohr } else { 641fca58076SAndreas Gohr $log = array('message' => 'failed ' . $this->getLang('token_mismatch'), 'user' => $user); 642fca58076SAndreas Gohr } 643fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 644fca58076SAndreas Gohr return; 645fca58076SAndreas Gohr } 646fca58076SAndreas Gohr // No GA OTP was supplied. 647fca58076SAndreas Gohr // If the user has no modules available, then grant access. 648fca58076SAndreas Gohr // The action preprocessing will send the user to the profile if needed. 649fca58076SAndreas Gohr $available = count($this->tokenMods) + count($this->otpMods) > 0; 650fca58076SAndreas Gohr $this->log('twofactor_before_auth_check: Tokens:' . count($this->tokenMods) . ' Codes:' . count($this->otpMods) . " Available:" . (int)$available, 651fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 652fca58076SAndreas Gohr if (!$available) { 653fca58076SAndreas Gohr // The user could not authenticate if they wanted to. 654fca58076SAndreas Gohr // Set this so they don't get auth prompted while setting up 2FA. 655fca58076SAndreas Gohr $this->_grant_clearance($user); 656fca58076SAndreas Gohr return; 657fca58076SAndreas Gohr } 658fca58076SAndreas Gohr // At this point, the user has a working module. 659fca58076SAndreas Gohr // If the only working module is for a token, then fail. 660fca58076SAndreas Gohr if (count($this->otpMods) == 0) { 661fca58076SAndreas Gohr msg($this->getLang('mustusetoken'), -1); 662fca58076SAndreas Gohr $event->preventDefault(); 663fca58076SAndreas Gohr return; 664fca58076SAndreas Gohr } 665fca58076SAndreas Gohr // The user is logged in to auth, but not into twofactor. 666fca58076SAndreas Gohr // The redirection handler will send the user to the twofactor login. 667fca58076SAndreas Gohr return; 668fca58076SAndreas Gohr } 669fca58076SAndreas Gohr 670fca58076SAndreas Gohr /** 671fca58076SAndreas Gohr * @param $event 672fca58076SAndreas Gohr * @param $param 673fca58076SAndreas Gohr */ 674fca58076SAndreas Gohr function twofactor_after_auth_check(&$event, $param) 675fca58076SAndreas Gohr { 676fca58076SAndreas Gohr global $ACT; 677fca58076SAndreas Gohr global $INPUT; 678fca58076SAndreas Gohr $this->log("twofactor_after_auth_check: start", self::LOGGING_DEBUG); 679fca58076SAndreas Gohr // Check if the action was login. 680fca58076SAndreas Gohr if ($ACT == 'login') { 681fca58076SAndreas Gohr // If there *was* no one logged in, then purge 2FA tokens. 682fca58076SAndreas Gohr if ($INPUT->server->str('REMOTE_USER', '') == '') { 683fca58076SAndreas Gohr $this->_logout(); 684fca58076SAndreas Gohr // If someone *just* logged in, then fire off a log. 685fca58076SAndreas Gohr if ($event->data['user']) { 686fca58076SAndreas Gohr // Send loglog an event to show the user logged in but needs OTP code. 687fca58076SAndreas Gohr $log = array( 688fca58076SAndreas Gohr 'message' => 'logged in, ' . $this->getLang('requires_otp'), 689fca58076SAndreas Gohr 'user' => $event->data['user'], 690fca58076SAndreas Gohr ); 691fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 692fca58076SAndreas Gohr } 693fca58076SAndreas Gohr return; 694fca58076SAndreas Gohr } 695fca58076SAndreas Gohr } 696fca58076SAndreas Gohr // Update helper variables here since we are logged in. 697fca58076SAndreas Gohr $this->_setHelperVariables(); 698fca58076SAndreas Gohr // If set, then send login notification and clear flag. 699fca58076SAndreas Gohr if ($_SESSION[DOKU_COOKIE]['twofactor_notify'] == true) { 700fca58076SAndreas Gohr // Set the clear flag if no messages can be sent or if the result is not false. 701fca58076SAndreas Gohr $clear = count($this_ > otpMods) > 0 || $this->_send_login_notification() !== false; 702fca58076SAndreas Gohr if ($clear) { 703fca58076SAndreas Gohr unset($_SESSION[DOKU_COOKIE]['twofactor_notify']); 704fca58076SAndreas Gohr } 705fca58076SAndreas Gohr } 706fca58076SAndreas Gohr return; 707fca58076SAndreas Gohr } 708fca58076SAndreas Gohr 709fca58076SAndreas Gohr /* Returns action to take. */ 710fca58076SAndreas Gohr private function _process_otp(&$event, $param) 711fca58076SAndreas Gohr { 712fca58076SAndreas Gohr global $ACT, $ID, $INPUT; 713fca58076SAndreas Gohr $this->log("_process_otp: start", self::LOGGING_DEBUG); 714fca58076SAndreas Gohr // Get the logged in user. 715fca58076SAndreas Gohr $user = $INPUT->server->str('REMOTE_USER'); 716fca58076SAndreas Gohr // See if the user is quitting OTP. We don't call it logoff because we don't want the user to think they are logged in! 717fca58076SAndreas Gohr // This has to be checked before the template is started. 718fca58076SAndreas Gohr if ($INPUT->has('otpquit')) { 719fca58076SAndreas Gohr // Send loglog an event to show the user aborted 2FA. 720fca58076SAndreas Gohr $log = array('message' => 'logged off, ' . $this->getLang('quit_otp'), 'user' => $user); 721fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 722fca58076SAndreas Gohr // Redirect to logout. 723fca58076SAndreas Gohr return 'logout'; 724fca58076SAndreas Gohr } 725fca58076SAndreas Gohr // Check if the user asked to generate and resend the OTP. 726fca58076SAndreas Gohr if ($INPUT->has('resend')) { 727fca58076SAndreas Gohr if ($INPUT->has('useall')) { 728fca58076SAndreas Gohr $defaultMod = null; 729fca58076SAndreas Gohr } else { 730fca58076SAndreas Gohr $defaultMod = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor", 731fca58076SAndreas Gohr "defaultmod") : null; 732fca58076SAndreas Gohr } 733fca58076SAndreas Gohr // At this point, try to send the OTP. 734fca58076SAndreas Gohr $mod = array_key_exists($defaultMod, $this->otpMods) ? $this->otpMods[$defaultMod] : null; 735fca58076SAndreas Gohr $this->_send_otp($mod); 736fca58076SAndreas Gohr return; 737fca58076SAndreas Gohr } 738fca58076SAndreas Gohr // If a OTP has been submitted by the user, then verify the OTP. 739fca58076SAndreas Gohr // If verified, then grant clearance and continue normally. 740fca58076SAndreas Gohr $otp = $INPUT->str('otpcode'); 741fca58076SAndreas Gohr if ($otp) { 742fca58076SAndreas Gohr foreach ($this->otpMods as $mod) { 743fca58076SAndreas Gohr $result = $mod->processLogin($otp); 744fca58076SAndreas Gohr if ($result) { 745fca58076SAndreas Gohr // The OTP code was valid. 746fca58076SAndreas Gohr $this->_grant_clearance(); 747fca58076SAndreas Gohr // Send loglog an event to show the user passed 2FA. 748fca58076SAndreas Gohr $log = array('message' => 'logged in ' . $this->getLang('otp_ok'), 'user' => $user); 749fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 750fca58076SAndreas Gohr /* 751fca58076SAndreas Gohr // This bypasses sending any further events to other modules for the login we stole earlier. 752fca58076SAndreas Gohr return 'show'; 753fca58076SAndreas Gohr */ 754fca58076SAndreas Gohr // This will trigger the login events again. However, this is to ensure 755fca58076SAndreas Gohr // that other modules work correctly because we hijacked this event earlier. 756fca58076SAndreas Gohr return 'login'; 757fca58076SAndreas Gohr } 758fca58076SAndreas Gohr } 759fca58076SAndreas Gohr // Send loglog an event to show the user entered the wrong OTP code. 760fca58076SAndreas Gohr $log = array('message' => 'failed OTP login, ' . $this->getLang('otp_mismatch'), 'user' => $user); 761fca58076SAndreas Gohr trigger_event('PLUGIN_LOGLOG_LOG', $log); 762fca58076SAndreas Gohr msg($this->getLang('twofactor_invalidotp'), -1); 763fca58076SAndreas Gohr } 764fca58076SAndreas Gohr return; 765fca58076SAndreas Gohr } 766fca58076SAndreas Gohr 767fca58076SAndreas Gohr /** 768fca58076SAndreas Gohr * Process any updates to two factor settings. 769fca58076SAndreas Gohr */ 770fca58076SAndreas Gohr private function _process_changes(&$event, $param) 771fca58076SAndreas Gohr { 772fca58076SAndreas Gohr // If the plugin is disabled, then exit. 773fca58076SAndreas Gohr $this->log("_process_changes: start", self::LOGGING_DEBUG); 774fca58076SAndreas Gohr $changed = false; 775fca58076SAndreas Gohr global $INPUT, $USERINFO, $conf, $auth, $lang, $ACT; 776fca58076SAndreas Gohr if (!$INPUT->has('save')) { 777fca58076SAndreas Gohr return; 778fca58076SAndreas Gohr } 779fca58076SAndreas Gohr // In needed, verify password. 780fca58076SAndreas Gohr if ($conf['profileconfirm']) { 781fca58076SAndreas Gohr if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) { 782fca58076SAndreas Gohr msg($lang['badpassconfirm'], -1); 783fca58076SAndreas Gohr return; 784fca58076SAndreas Gohr } 785fca58076SAndreas Gohr } 786fca58076SAndreas Gohr // Process opt in/out. 787fca58076SAndreas Gohr if ($this->getConf("optinout") != 'mandatory') { 788fca58076SAndreas Gohr $oldoptinout = $this->attribute->get("twofactor", "state"); 789fca58076SAndreas Gohr $optinout = $INPUT->bool('optinout', false) ? 'in' : 'out'; 790fca58076SAndreas Gohr if ($oldoptinout != $optinout) { 791fca58076SAndreas Gohr $this->attribute->set("twofactor", "state", $optinout); 792fca58076SAndreas Gohr $changed = true; 793fca58076SAndreas Gohr } 794fca58076SAndreas Gohr } 795fca58076SAndreas Gohr // Process notifications. 796fca58076SAndreas Gohr if ($this->getConf("loginnotice") == 'user') { 797fca58076SAndreas Gohr $oldloginnotice = $this->attribute->get("twofactor", "loginnotice"); 798fca58076SAndreas Gohr $loginnotice = $INPUT->bool('loginnotice', false); 799fca58076SAndreas Gohr if ($oldloginnotice != $loginnotice) { 800fca58076SAndreas Gohr $this->attribute->set("twofactor", "loginnotice", $loginnotice); 801fca58076SAndreas Gohr $changed = true; 802fca58076SAndreas Gohr } 803fca58076SAndreas Gohr } 804fca58076SAndreas Gohr // Process default module. 805fca58076SAndreas Gohr $defaultmodule = $INPUT->str('default_module', ''); 806fca58076SAndreas Gohr if ($defaultmodule) { 807fca58076SAndreas Gohr if ($defaultmodule === $this->getLang('useallotp')) { 808fca58076SAndreas Gohr // Set to use ALL OTP channels. 809fca58076SAndreas Gohr $this->attribute->set("twofactor", "defaultmod", null); 810fca58076SAndreas Gohr $changed = true; 811fca58076SAndreas Gohr } else { 812fca58076SAndreas Gohr $useableMods = array(); 813fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 814fca58076SAndreas Gohr if (!$mod->canAuthLogin() && $mod->canUse()) { 815fca58076SAndreas Gohr $useableMods[$mod->getLang("name")] = $mod; 816fca58076SAndreas Gohr } 817fca58076SAndreas Gohr } 818fca58076SAndreas Gohr if (array_key_exists($defaultmodule, $useableMods)) { 819fca58076SAndreas Gohr $this->attribute->set("twofactor", "defaultmod", $defaultmodule); 820fca58076SAndreas Gohr $changed = true; 821fca58076SAndreas Gohr } 822fca58076SAndreas Gohr } 823fca58076SAndreas Gohr } 824fca58076SAndreas Gohr // Update module settings. 825fca58076SAndreas Gohr $sendotp = null; 826fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 827fca58076SAndreas Gohr $this->log('_process_changes: processing ' . get_class($mod) . '::processProfileForm()', 828fca58076SAndreas Gohr self::LOGGING_DEBUG); 829fca58076SAndreas Gohr $result = $mod->processProfileForm(); 830fca58076SAndreas Gohr $this->log('_process_changes: processing ' . get_class($mod) . '::processProfileForm() == ' . $result, 831fca58076SAndreas Gohr self::LOGGING_DEBUGPLUS); 832fca58076SAndreas Gohr // false:change failed 'failed':OTP failed null: no change made 833fca58076SAndreas Gohr $changed |= $result !== false && $result !== 'failed' && $result !== null; 834fca58076SAndreas Gohr switch ((string)$result) { 835fca58076SAndreas Gohr case 'verified': 836fca58076SAndreas Gohr // Remove used OTP. 837fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp"); 838fca58076SAndreas Gohr msg($mod->getLang('passedsetup'), 1); 839fca58076SAndreas Gohr // Reset helper variables. 840fca58076SAndreas Gohr $this->_setHelperVariables(); 841fca58076SAndreas Gohr $this->log("2FA Added: " . $INPUT->server->str('REMOTE_USER', '') . ' ' . get_class($mod), 842fca58076SAndreas Gohr self::LOGGING_AUDIT); 843fca58076SAndreas Gohr break; 844fca58076SAndreas Gohr case 'failed': 845fca58076SAndreas Gohr msg($mod->getLang('failedsetup'), -1); 846fca58076SAndreas Gohr break; 847fca58076SAndreas Gohr case 'otp': 848fca58076SAndreas Gohr if (!$sendotp) { 849fca58076SAndreas Gohr $sendotp = $mod; 850fca58076SAndreas Gohr } 851fca58076SAndreas Gohr break; 852fca58076SAndreas Gohr case 'deleted': 853fca58076SAndreas Gohr $this->log("2FA Removed: " . $INPUT->server->str('REMOTE_USER', '') . ' ' . get_class($mod), 854fca58076SAndreas Gohr self::LOGGING_AUDIT); 855fca58076SAndreas Gohr // Reset helper variables. 856fca58076SAndreas Gohr $this->_setHelperVariables(); 857fca58076SAndreas Gohr break; 858fca58076SAndreas Gohr } 859fca58076SAndreas Gohr } 860fca58076SAndreas Gohr // Send OTP if requested. 861fca58076SAndreas Gohr if (is_object($sendotp)) { 862fca58076SAndreas Gohr // Force the message since it will fail the canUse function. 863fca58076SAndreas Gohr if ($this->_send_otp($sendotp, true)) { 864fca58076SAndreas Gohr msg($sendotp->getLang('needsetup'), 1); 865fca58076SAndreas Gohr } else { 866fca58076SAndreas Gohr msg("Could not send message using " . get_class($sendotp), -1); 867fca58076SAndreas Gohr } 868fca58076SAndreas Gohr } 869fca58076SAndreas Gohr // Update change status if changed. 870fca58076SAndreas Gohr if ($changed) { 871fca58076SAndreas Gohr // If there were any changes, update the available tokens accordingly. 872fca58076SAndreas Gohr $this->_setHelperVariables(); 873fca58076SAndreas Gohr msg($this->getLang('updated'), 1); 874fca58076SAndreas Gohr } 875fca58076SAndreas Gohr return true; 876fca58076SAndreas Gohr } 877fca58076SAndreas Gohr 878fca58076SAndreas Gohr /** 879fca58076SAndreas Gohr * Handles the email and text OTP options. 880fca58076SAndreas Gohr * NOTE: The user will be technically logged in at this point. This module will rewrite the 881fca58076SAndreas Gohr * page with the prompt for the OTP until validated or the user logs out. 882fca58076SAndreas Gohr */ 883fca58076SAndreas Gohr function twofactor_otp_login(&$event, $param) 884fca58076SAndreas Gohr { 885fca58076SAndreas Gohr $this->log("twofactor_otp_login: start", self::LOGGING_DEBUG); 886fca58076SAndreas Gohr // Skip this if not logged in or already two factor authenticated. 887fca58076SAndreas Gohr // Ensure the OTP exists and is still valid. If we need to, send a OTP. 888fca58076SAndreas Gohr $otpQuery = $this->get_otp_code(); 889fca58076SAndreas Gohr if ($otpQuery == false) { 890fca58076SAndreas Gohr $useableMods = array(); 891fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 892fca58076SAndreas Gohr if (!$mod->canAuthLogin() && $mod->canUse()) { 893fca58076SAndreas Gohr $useableMods[$mod->getLang("name")] = $mod; 894fca58076SAndreas Gohr } 895fca58076SAndreas Gohr } 896fca58076SAndreas Gohr $defaultMod = $this->attribute->exists("twofactor", "defaultmod") ? $this->attribute->get("twofactor", 897fca58076SAndreas Gohr "defaultmod") : null; 898fca58076SAndreas Gohr $mod = array_key_exists($defaultMod, $useableMods) ? $useableMods[$defaultMod] : null; 899fca58076SAndreas Gohr $this->_send_otp($mod); 900fca58076SAndreas Gohr } 901fca58076SAndreas Gohr // Generate the form to login. 902fca58076SAndreas Gohr // If we are here, then only provide options to accept the OTP or to logout. 903fca58076SAndreas Gohr global $lang; 904fca58076SAndreas Gohr $form = new Doku_Form(array('id' => 'otp_setup')); 905fca58076SAndreas Gohr $form->startFieldset($this->getLang('twofactor_otplogin')); 906fca58076SAndreas Gohr $form->addElement(form_makeTextField('otpcode', '', $this->getLang('twofactor_otplogin'), '', 'block', 907fca58076SAndreas Gohr array('size' => '50', 'autocomplete' => 'off'))); 908fca58076SAndreas Gohr $form->addElement(form_makeButton('submit', '', $this->getLang('btn_login'))); 909fca58076SAndreas Gohr $form->addElement(form_makeTag('br')); 910fca58076SAndreas Gohr $form->addElement(form_makeCheckboxField('useall', '1', $this->getLang('twofactor_useallmods'), '', 'block')); 911fca58076SAndreas Gohr $form->addElement(form_makeTag('br')); 912fca58076SAndreas Gohr $form->addElement(form_makeButton('submit', '', $this->getLang('btn_resend'), array('name' => 'resend'))); 913fca58076SAndreas Gohr $form->addElement(form_makeButton('submit', '', $this->getLang('btn_quit'), array('name' => 'otpquit'))); 914fca58076SAndreas Gohr $form->endFieldset(); 915fca58076SAndreas Gohr echo '<div class="centeralign">' . NL . $form->getForm() . '</div>' . NL; 916fca58076SAndreas Gohr } 917fca58076SAndreas Gohr 918fca58076SAndreas Gohr /** 919fca58076SAndreas Gohr * Sends a message using configured modules. 920fca58076SAndreas Gohr * If $module is set to a specific instance, that instance will be used to 921fca58076SAndreas Gohr * send the message. If not supplied or null, then all configured modules 922fca58076SAndreas Gohr * will be used to send the message. $module can also be an array of 923fca58076SAndreas Gohr * selected modules. 924fca58076SAndreas Gohr * If $force is true, then will try to send the message even if the module 925fca58076SAndreas Gohr * has not been validated. 926fca58076SAndreas Gohr * @return array(array, mixed) - The first item in the array is an array 927fca58076SAndreas Gohr * of all modules that the message was successfully sent by. The 928fca58076SAndreas Gohr * second item is true if successfull to all attempted tramsmission 929fca58076SAndreas Gohr * modules, false if all failed, and a number of how many successes 930fca58076SAndreas Gohr * if only some modules failed. 931fca58076SAndreas Gohr */ 932fca58076SAndreas Gohr private function _send_message($subject, $message, $module = null, $force = false) 933fca58076SAndreas Gohr { 934fca58076SAndreas Gohr global $INPUT; 935fca58076SAndreas Gohr $this->log("_send_message: start", self::LOGGING_DEBUG); 936fca58076SAndreas Gohr if ($module === null) { 937fca58076SAndreas Gohr $module = $this->otpMods; 938fca58076SAndreas Gohr } 939fca58076SAndreas Gohr if (!is_array($module)) { 940fca58076SAndreas Gohr $module = array($module); 941fca58076SAndreas Gohr } 942fca58076SAndreas Gohr if (count($module) >= 1) { 943fca58076SAndreas Gohr $modulekeys = array_keys($module); 944fca58076SAndreas Gohr $modulekey = $modulekeys[0]; 945fca58076SAndreas Gohr $modname = get_class($module[$modulekey]); 946fca58076SAndreas Gohr } else { 947fca58076SAndreas Gohr $modname = null; 948fca58076SAndreas Gohr } 949fca58076SAndreas Gohr // Attempt to deliver messages. 950fca58076SAndreas Gohr $user = $INPUT->server->str('REMOTE_USER', '*unknown*'); 951fca58076SAndreas Gohr $success = 0; 952fca58076SAndreas Gohr $modname = array(); 953fca58076SAndreas Gohr foreach ($module as $mod) { 954fca58076SAndreas Gohr if ($mod->canTransmitMessage()) { 955fca58076SAndreas Gohr $worked = $mod->transmitMessage($subject, $message, $force); 956fca58076SAndreas Gohr if ($worked) { 957fca58076SAndreas Gohr $success += 1; 958fca58076SAndreas Gohr $modname[] = get_class($mod); 959fca58076SAndreas Gohr } 960fca58076SAndreas Gohr $this->log("Message " . ($worked ? '' : 'not ') . "sent to $user via " . get_class($mod), 961fca58076SAndreas Gohr self::LOGGING_AUDITPLUS); 962fca58076SAndreas Gohr } 963fca58076SAndreas Gohr } 964fca58076SAndreas Gohr return array($modname, $success == 0 ? false : ($success == count($module) ? true : $success)); 965fca58076SAndreas Gohr } 966fca58076SAndreas Gohr 967fca58076SAndreas Gohr /** 968fca58076SAndreas Gohr * Transmits a One-Time Password (OTP) using configured modules. 969fca58076SAndreas Gohr * If $module is set to a specific instance, that instance will be used to 970fca58076SAndreas Gohr * send the OTP. If not supplied or null, then all configured modules will 971fca58076SAndreas Gohr * be used to send the OTP. $module can also be an array of selected 972fca58076SAndreas Gohr * modules. 973fca58076SAndreas Gohr * If $force is true, then will try to send the message even if the module 974fca58076SAndreas Gohr * has not been validated. 975fca58076SAndreas Gohr * @return mixed - true if successfull to all attempted tramsmission 976fca58076SAndreas Gohr * modules, false if all failed, and a number of how many successes 977fca58076SAndreas Gohr * if only some modules failed. 978fca58076SAndreas Gohr */ 979fca58076SAndreas Gohr private function _send_otp($module = null, $force = false) 980fca58076SAndreas Gohr { 981fca58076SAndreas Gohr $this->log("_send_otp: start", self::LOGGING_DEBUG); 982fca58076SAndreas Gohr // Generate the OTP code. 983fca58076SAndreas Gohr $characters = '0123456789'; 984fca58076SAndreas Gohr $otp = ''; 985fca58076SAndreas Gohr for ($index = 0; $index < $this->getConf('otplength'); ++$index) { 986fca58076SAndreas Gohr $otp .= $characters[rand(0, strlen($characters) - 1)]; 987fca58076SAndreas Gohr } 988fca58076SAndreas Gohr // Create the subject. 989fca58076SAndreas Gohr $subject = $this->getConf('otpsubject'); 990fca58076SAndreas Gohr // Create the message. 991fca58076SAndreas Gohr $message = str_replace('$otp', $otp, $this->getConf('otpcontent')); 992fca58076SAndreas Gohr // Attempt to deliver the message. 993fca58076SAndreas Gohr list($modname, $result) = $this->_send_message($subject, $message, $module, $force); 994fca58076SAndreas Gohr // If partially successful, store the OTP code and the timestamp the OTP expires at. 995fca58076SAndreas Gohr if ($result) { 996fca58076SAndreas Gohr $otpData = array($otp, time() + $this->getConf('sentexpiry') * 60, $modname); 997fca58076SAndreas Gohr if (!$this->attribute->set("twofactor", "otp", $otpData)) { 998fca58076SAndreas Gohr msg("Unable to record OTP for later use.", -1); 999fca58076SAndreas Gohr } 1000fca58076SAndreas Gohr } 1001fca58076SAndreas Gohr return $result; 1002fca58076SAndreas Gohr } 1003fca58076SAndreas Gohr 1004fca58076SAndreas Gohr /** 1005fca58076SAndreas Gohr * Returns the OTP code sent to the user, if it has not expired. 1006fca58076SAndreas Gohr * @return mixed - false if there is no unexpired OTP, otherwise 1007fca58076SAndreas Gohr * array of the OTP and the modules that successfully sent it. 1008fca58076SAndreas Gohr */ 1009fca58076SAndreas Gohr public function get_otp_code() 1010fca58076SAndreas Gohr { 1011fca58076SAndreas Gohr $this->log("get_otp_code: start", self::LOGGING_DEBUG); 1012fca58076SAndreas Gohr $otpQuery = $this->attribute->get("twofactor", "otp", $success); 1013fca58076SAndreas Gohr if (!$success) { 1014fca58076SAndreas Gohr return false; 1015fca58076SAndreas Gohr } 1016fca58076SAndreas Gohr list($otp, $expiry, $modname) = $otpQuery; 1017fca58076SAndreas Gohr if (time() > $expiry) { 1018fca58076SAndreas Gohr $this->attribute->del("twofactor", "otp"); 1019fca58076SAndreas Gohr return false; 1020fca58076SAndreas Gohr } 1021fca58076SAndreas Gohr return array($otp, $modname); 1022fca58076SAndreas Gohr } 1023fca58076SAndreas Gohr 1024fca58076SAndreas Gohr private function _setHelperVariables($user = null) 1025fca58076SAndreas Gohr { 1026fca58076SAndreas Gohr $this->log("_setHelperVariables: start", self::LOGGING_DEBUGPLUS); 1027fca58076SAndreas Gohr $tokenMods = array(); 1028fca58076SAndreas Gohr $otpMods = array(); 1029fca58076SAndreas Gohr $state = $this->attribute->get("twofactor", "state"); 1030fca58076SAndreas Gohr $optinout = $this->getConf("optinout"); 1031fca58076SAndreas Gohr $enabled = $optinout == 'mandatory' || ($state == '' ? $optinout == 'optin' : $state == 'in'); 1032fca58076SAndreas Gohr $this->log("_setHelperVariables: " . print_r(array($optinout, $state, $enabled), true), self::LOGGING_DEBUG); 1033fca58076SAndreas Gohr // Skip if not enabled for user 1034fca58076SAndreas Gohr if ($enabled) { 1035fca58076SAndreas Gohr // List all working token modules (GA, RSA, etc.). 1036fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 1037fca58076SAndreas Gohr if ($mod->canAuthLogin() && $mod->canUse($user)) { 1038fca58076SAndreas Gohr $this->log('Can use ' . get_class($mod) . ' for tokens', self::LOGGING_DEBUG); 1039fca58076SAndreas Gohr $tokenMods[$mod->getLang("name")] = $mod; 1040fca58076SAndreas Gohr } else { 1041fca58076SAndreas Gohr $this->log('Can NOT use ' . get_class($mod) . ' for tokens', self::LOGGING_DEBUG); 1042fca58076SAndreas Gohr } 1043fca58076SAndreas Gohr } 1044fca58076SAndreas Gohr // List all working OTP modules (SMS, Twilio, etc.). 1045fca58076SAndreas Gohr foreach ($this->modules as $name => $mod) { 1046fca58076SAndreas Gohr if (!$mod->canAuthLogin() && $mod->canUse($user)) { 1047fca58076SAndreas Gohr $this->log('Can use ' . get_class($mod) . ' for otp', self::LOGGING_DEBUG); 1048fca58076SAndreas Gohr $otpMods[$mod->getLang("name")] = $mod; 1049fca58076SAndreas Gohr } else { 1050fca58076SAndreas Gohr $this->log('Can NOT use ' . get_class($mod) . ' for otp', self::LOGGING_DEBUG); 1051fca58076SAndreas Gohr } 1052fca58076SAndreas Gohr } 1053fca58076SAndreas Gohr } 1054fca58076SAndreas Gohr $this->tokenMods = $tokenMods; 1055fca58076SAndreas Gohr $this->otpMods = $otpMods; 1056fca58076SAndreas Gohr } 1057fca58076SAndreas Gohr 1058fca58076SAndreas Gohr const LOGGING_AUDIT = 1; // Audit records 2FA login and logout activity. 1059fca58076SAndreas Gohr const LOGGING_AUDITPLUS = 2; // Audit+ also records sending of notifications. 1060fca58076SAndreas Gohr const LOGGING_DEBUG = 3; // Debug provides detailed workflow data. 1061fca58076SAndreas Gohr const LOGGING_DEBUGPLUS = 4; // Debug+ also includes variables passed to and from functions. 1062fca58076SAndreas Gohr 1063fca58076SAndreas Gohr public function log($message, $level = 1) 1064fca58076SAndreas Gohr { 1065fca58076SAndreas Gohr // If the log level requested is below audit or greater than what is permitted in the configuration, then exit. 1066fca58076SAndreas Gohr if ($level < self::LOGGING_AUDIT || $level > $this->getConf('logging_level')) { 1067fca58076SAndreas Gohr return; 1068fca58076SAndreas Gohr } 1069fca58076SAndreas Gohr global $conf; 1070fca58076SAndreas Gohr // Always purge line containing "[pass]". 1071fca58076SAndreas Gohr $message = implode("\n", array_filter(explode("\n", $message), function ($x) { 1072fca58076SAndreas Gohr return !strstr($x, '[pass]'); 1073fca58076SAndreas Gohr })); 1074fca58076SAndreas Gohr // If DEBUGPLUS, then append the trace log. 1075fca58076SAndreas Gohr if ($level == self::LOGGING_DEBUGPLUS) { 1076fca58076SAndreas Gohr $e = new Exception(); 1077fca58076SAndreas Gohr $message .= "\n" . print_r(str_replace(DOKU_REL, '', $e->getTraceAsString()), true); 1078fca58076SAndreas Gohr } 1079fca58076SAndreas Gohr $logfile = $this->getConf('logging_path'); 1080fca58076SAndreas Gohr $logfile = substr($logfile, 0, 1) == '/' ? $logfile : DOKU_INC . $conf['savedir'] . '/' . $logfile; 1081fca58076SAndreas Gohr io_lock($logfile); 1082fca58076SAndreas Gohr #open for append logfile 1083fca58076SAndreas Gohr $handle = @fopen($logfile, 'at'); 1084fca58076SAndreas Gohr if ($handle) { 1085fca58076SAndreas Gohr $date = date(DATE_RFC2822); 1086fca58076SAndreas Gohr $IP = $_SERVER["REMOTE_ADDR"]; 1087fca58076SAndreas Gohr $id = session_id(); 1088fca58076SAndreas Gohr fwrite($handle, "$date,$id,$IP,$level,\"$message\"\n"); 1089fca58076SAndreas Gohr fclose($handle); 1090fca58076SAndreas Gohr } 1091fca58076SAndreas Gohr #write "date level message" 1092fca58076SAndreas Gohr io_unlock($logfile); 1093fca58076SAndreas Gohr } 1094a386a536SAndreas Gohr 1095a386a536SAndreas Gohr // endregion 1096fca58076SAndreas Gohr} 1097