142a27035SAndreas Gohr<?php 218622736SAndreas Gohr 309b1e97eSAndreas Gohruse dokuwiki\Extension\ActionPlugin; 409b1e97eSAndreas Gohruse dokuwiki\Extension\Event; 5*5e79b2eeSAndreas Gohruse dokuwiki\Extension\EventHandler; 609b1e97eSAndreas Gohruse dokuwiki\Form\Form; 7c6d794b3SAndreas Gohruse dokuwiki\plugin\captcha\FileCookie; 8969b14c4SAndreas Gohruse dokuwiki\plugin\captcha\IpCounter; 9969b14c4SAndreas Gohr 1042a27035SAndreas Gohr/** 1142a27035SAndreas Gohr * CAPTCHA antispam plugin 1242a27035SAndreas Gohr * 1342a27035SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 1442a27035SAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 1542a27035SAndreas Gohr */ 1609b1e97eSAndreas Gohrclass action_plugin_captcha extends ActionPlugin 171c08a51cSAndreas Gohr{ 1842a27035SAndreas Gohr /** 1942a27035SAndreas Gohr * register the eventhandlers 2042a27035SAndreas Gohr */ 2109b1e97eSAndreas Gohr public function register(EventHandler $controller) 221c08a51cSAndreas Gohr { 237218f96cSAndreas Gohr // check CAPTCHA success 24*5e79b2eeSAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleCaptchaInput', []); 2542a27035SAndreas Gohr 267218f96cSAndreas Gohr // inject in edit form 27*5e79b2eeSAndreas Gohr $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handleFormOutput', []); 2842a27035SAndreas Gohr 297218f96cSAndreas Gohr // inject in user registration 30*5e79b2eeSAndreas Gohr $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handleFormOutput', []); 31f74276b8SAndreas Gohr 32f74276b8SAndreas Gohr // inject in password reset 33*5e79b2eeSAndreas Gohr $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handleFormOutput', []); 34643f15bdSAndreas Gohr 35643f15bdSAndreas Gohr // inject in login form 36*5e79b2eeSAndreas Gohr $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handleFormOutput', []); 371c08a51cSAndreas Gohr 38643f15bdSAndreas Gohr // check on login 39*5e79b2eeSAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handleLogin', []); 40cde3ece1SAndreas Gohr 41cde3ece1SAndreas Gohr // clean up captcha cookies 42*5e79b2eeSAndreas Gohr $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handleIndexer', []); 43969b14c4SAndreas Gohr 44969b14c4SAndreas Gohr // log authentication failures 45969b14c4SAndreas Gohr if ((int)$this->getConf('loginprotect') > 1) { 46*5e79b2eeSAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleAuth', []); 47969b14c4SAndreas Gohr } 4842a27035SAndreas Gohr } 4942a27035SAndreas Gohr 5042a27035SAndreas Gohr /** 51bd26d35bSAndreas Gohr * Check if the current mode should be handled by CAPTCHA 52bd26d35bSAndreas Gohr * 53f74276b8SAndreas Gohr * Note: checking needs to be done when a form has been submitted, not when the form 54f74276b8SAndreas Gohr * is shown for the first time. Except for the editing process this is not determined 55f74276b8SAndreas Gohr * by $act alone but needs to inspect other input variables. 56f74276b8SAndreas Gohr * 57bd26d35bSAndreas Gohr * @param string $act cleaned action mode 58bd26d35bSAndreas Gohr * @return bool 59bd26d35bSAndreas Gohr */ 60*5e79b2eeSAndreas Gohr protected function needsChecking($act) 611c08a51cSAndreas Gohr { 62bd26d35bSAndreas Gohr global $INPUT; 63bd26d35bSAndreas Gohr 64bd26d35bSAndreas Gohr switch ($act) { 65bd26d35bSAndreas Gohr case 'save': 66bd26d35bSAndreas Gohr return true; 67bd26d35bSAndreas Gohr case 'register': 68f74276b8SAndreas Gohr case 'resendpwd': 69bd26d35bSAndreas Gohr return $INPUT->bool('save'); 70643f15bdSAndreas Gohr case 'login': 71643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 72bd26d35bSAndreas Gohr default: 73bd26d35bSAndreas Gohr return false; 74bd26d35bSAndreas Gohr } 75bd26d35bSAndreas Gohr } 76bd26d35bSAndreas Gohr 77bd26d35bSAndreas Gohr /** 78bd26d35bSAndreas Gohr * Aborts the given mode 79bd26d35bSAndreas Gohr * 80bd26d35bSAndreas Gohr * Aborting depends on the mode. It might unset certain input parameters or simply switch 81bd26d35bSAndreas Gohr * the mode to something else (giving as return which needs to be passed back to the 82bd26d35bSAndreas Gohr * ACTION_ACT_PREPROCESS event) 83bd26d35bSAndreas Gohr * 84bd26d35bSAndreas Gohr * @param string $act cleaned action mode 85bd26d35bSAndreas Gohr * @return string the new mode to use 86bd26d35bSAndreas Gohr */ 87*5e79b2eeSAndreas Gohr protected function abortAction($act) 881c08a51cSAndreas Gohr { 89bd26d35bSAndreas Gohr global $INPUT; 90bd26d35bSAndreas Gohr 91bd26d35bSAndreas Gohr switch ($act) { 92bd26d35bSAndreas Gohr case 'save': 93bd26d35bSAndreas Gohr return 'preview'; 94bd26d35bSAndreas Gohr case 'register': 95f74276b8SAndreas Gohr case 'resendpwd': 96bd26d35bSAndreas Gohr $INPUT->post->set('save', false); 97f74276b8SAndreas Gohr return $act; 98643f15bdSAndreas Gohr case 'login': 99643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 100bd26d35bSAndreas Gohr default: 101bd26d35bSAndreas Gohr return $act; 102bd26d35bSAndreas Gohr } 103bd26d35bSAndreas Gohr } 104bd26d35bSAndreas Gohr 105bd26d35bSAndreas Gohr /** 106969b14c4SAndreas Gohr * Should a login CAPTCHA be used? 107969b14c4SAndreas Gohr * 108969b14c4SAndreas Gohr * @return bool 109969b14c4SAndreas Gohr */ 110969b14c4SAndreas Gohr protected function protectLogin() 111969b14c4SAndreas Gohr { 112969b14c4SAndreas Gohr $config = (int)$this->getConf('loginprotect'); 113969b14c4SAndreas Gohr if ($config < 1) return false; // not wanted 114969b14c4SAndreas Gohr if ($config === 1) return true; // always wanted 115969b14c4SAndreas Gohr $count = (new IpCounter())->get(); 116969b14c4SAndreas Gohr return $count > 2; // only after 3 failed attempts 117969b14c4SAndreas Gohr } 118969b14c4SAndreas Gohr 119969b14c4SAndreas Gohr /** 120643f15bdSAndreas Gohr * Handles CAPTCHA check in login 121643f15bdSAndreas Gohr * 122643f15bdSAndreas Gohr * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them 123643f15bdSAndreas Gohr * in their own event. 124643f15bdSAndreas Gohr * 12509b1e97eSAndreas Gohr * @param Event $event 126643f15bdSAndreas Gohr * @param $param 127643f15bdSAndreas Gohr */ 128*5e79b2eeSAndreas Gohr public function handleLogin(Event $event, $param) 1291c08a51cSAndreas Gohr { 130643f15bdSAndreas Gohr global $INPUT; 131969b14c4SAndreas Gohr if (!$this->protectLogin()) return; // no protection wanted 132643f15bdSAndreas Gohr if (!$INPUT->bool('u')) return; // this login was not triggered by a form 133643f15bdSAndreas Gohr 134643f15bdSAndreas Gohr // we need to have $ID set for the captcha check 135643f15bdSAndreas Gohr global $ID; 136643f15bdSAndreas Gohr $ID = getID(); 137643f15bdSAndreas Gohr 138643f15bdSAndreas Gohr /** @var helper_plugin_captcha $helper */ 139643f15bdSAndreas Gohr $helper = plugin_load('helper', 'captcha'); 140643f15bdSAndreas Gohr if (!$helper->check()) { 141643f15bdSAndreas Gohr $event->data['silent'] = true; // we have our own message 142643f15bdSAndreas Gohr $event->result = false; // login fail 143643f15bdSAndreas Gohr $event->preventDefault(); 144643f15bdSAndreas Gohr $event->stopPropagation(); 145643f15bdSAndreas Gohr } 146643f15bdSAndreas Gohr } 147643f15bdSAndreas Gohr 148643f15bdSAndreas Gohr /** 149643f15bdSAndreas Gohr * Intercept all actions and check for CAPTCHA first. 15042a27035SAndreas Gohr */ 151*5e79b2eeSAndreas Gohr public function handleCaptchaInput(Event $event, $param) 1521c08a51cSAndreas Gohr { 15364382f29SAndreas Gohr global $INPUT; 15464382f29SAndreas Gohr 1557218f96cSAndreas Gohr $act = act_clean($event->data); 156*5e79b2eeSAndreas Gohr if (!$this->needsChecking($act)) return; 15793f66506SAndreas Gohr 15842a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 15964382f29SAndreas Gohr if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) { 16042a27035SAndreas Gohr return; 16142a27035SAndreas Gohr } 16242a27035SAndreas Gohr 16377e00bf9SAndreas Gohr // check captcha 1647218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 16577e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 16677e00bf9SAndreas Gohr if (!$helper->check()) { 167*5e79b2eeSAndreas Gohr $event->data = $this->abortAction($act); 16842a27035SAndreas Gohr } 16942a27035SAndreas Gohr } 17042a27035SAndreas Gohr 17142a27035SAndreas Gohr /** 172*5e79b2eeSAndreas Gohr * Inject the CAPTCHA in a \dokuwiki\Form\Form 17342a27035SAndreas Gohr */ 174*5e79b2eeSAndreas Gohr public function handleFormOutput(Event $event, $param) 1751c08a51cSAndreas Gohr { 17664382f29SAndreas Gohr global $INPUT; 17764382f29SAndreas Gohr 178*5e79b2eeSAndreas Gohr if ($event->name === 'FORM_LOGIN_OUTPUT' && !$this->protectLogin()) { 179c0439b03SAndreas Gohr // no login protection wanted 180c0439b03SAndreas Gohr return; 181c0439b03SAndreas Gohr } 18231c8e2bdSAndreas Gohr 18309b1e97eSAndreas Gohr /** @var Form|\Doku_Form $form */ 1841c08a51cSAndreas Gohr $form = $event->data; 1851c08a51cSAndreas Gohr 18647afabe6SAndreas Gohr // get position of submit button 1871c08a51cSAndreas Gohr $pos = $form->findPositionByAttribute('type', 'submit'); 18847afabe6SAndreas Gohr if (!$pos) return; // no button -> source view mode 18947afabe6SAndreas Gohr 19042a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 19164382f29SAndreas Gohr if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) { 19242a27035SAndreas Gohr return; 19342a27035SAndreas Gohr } 19442a27035SAndreas Gohr 19577e00bf9SAndreas Gohr // get the CAPTCHA 1967218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 19777e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 19877e00bf9SAndreas Gohr $out = $helper->getHTML(); 19947afabe6SAndreas Gohr 2001c08a51cSAndreas Gohr // insert before the submit button 2011c08a51cSAndreas Gohr $form->addHTML($out, $pos); 20242a27035SAndreas Gohr } 20342a27035SAndreas Gohr 204cde3ece1SAndreas Gohr /** 205cde3ece1SAndreas Gohr * Clean cookies once per day 206cde3ece1SAndreas Gohr */ 207*5e79b2eeSAndreas Gohr public function handleIndexer(Event $event, $param) 2081c08a51cSAndreas Gohr { 209cde3ece1SAndreas Gohr $lastrun = getCacheName('captcha', '.captcha'); 210cde3ece1SAndreas Gohr $last = @filemtime($lastrun); 211cde3ece1SAndreas Gohr if (time() - $last < 24 * 60 * 60) return; 212cde3ece1SAndreas Gohr 213c6d794b3SAndreas Gohr FileCookie::clean(); 2145d59bd09SAndreas Gohr touch($lastrun); 215cde3ece1SAndreas Gohr 216cde3ece1SAndreas Gohr $event->preventDefault(); 217cde3ece1SAndreas Gohr $event->stopPropagation(); 218cde3ece1SAndreas Gohr } 219969b14c4SAndreas Gohr 220969b14c4SAndreas Gohr /** 221969b14c4SAndreas Gohr * Count failed login attempts 222969b14c4SAndreas Gohr */ 223*5e79b2eeSAndreas Gohr public function handleAuth(Event $event, $param) 224969b14c4SAndreas Gohr { 225969b14c4SAndreas Gohr global $INPUT; 226969b14c4SAndreas Gohr $act = act_clean($event->data); 227969b14c4SAndreas Gohr if ( 228969b14c4SAndreas Gohr $act != 'logout' && 22931c8e2bdSAndreas Gohr $INPUT->str('u') !== '' && 230969b14c4SAndreas Gohr empty($INPUT->server->str('http_credentials')) && 231969b14c4SAndreas Gohr empty($INPUT->server->str('REMOTE_USER')) 232969b14c4SAndreas Gohr ) { 233969b14c4SAndreas Gohr // This is a failed authentication attempt, count it 234969b14c4SAndreas Gohr (new IpCounter())->increment(); 235969b14c4SAndreas Gohr } 236969b14c4SAndreas Gohr 237969b14c4SAndreas Gohr if ( 238969b14c4SAndreas Gohr $act == 'login' && 239969b14c4SAndreas Gohr !empty($INPUT->server->str('REMOTE_USER')) 240969b14c4SAndreas Gohr ) { 241969b14c4SAndreas Gohr // This is a successful login, reset the counter 242969b14c4SAndreas Gohr (new IpCounter())->reset(); 243969b14c4SAndreas Gohr } 244969b14c4SAndreas Gohr } 24542a27035SAndreas Gohr} 246