142a27035SAndreas Gohr<?php 218622736SAndreas Gohr 3969b14c4SAndreas Gohruse dokuwiki\plugin\captcha\IpCounter; 4969b14c4SAndreas Gohr 542a27035SAndreas Gohr/** 642a27035SAndreas Gohr * CAPTCHA antispam plugin 742a27035SAndreas Gohr * 842a27035SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 942a27035SAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 1042a27035SAndreas Gohr */ 111c08a51cSAndreas Gohrclass action_plugin_captcha extends DokuWiki_Action_Plugin 121c08a51cSAndreas Gohr{ 1342a27035SAndreas Gohr 1442a27035SAndreas Gohr /** 1542a27035SAndreas Gohr * register the eventhandlers 1642a27035SAndreas Gohr */ 171c08a51cSAndreas Gohr public function register(Doku_Event_Handler $controller) 181c08a51cSAndreas Gohr { 197218f96cSAndreas Gohr // check CAPTCHA success 201c08a51cSAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_captcha_input', []); 2142a27035SAndreas Gohr 227218f96cSAndreas Gohr // inject in edit form 231c08a51cSAndreas Gohr $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 241c08a51cSAndreas Gohr $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 2542a27035SAndreas Gohr 267218f96cSAndreas Gohr // inject in user registration 271c08a51cSAndreas Gohr $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 281c08a51cSAndreas Gohr $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 29f74276b8SAndreas Gohr 30f74276b8SAndreas Gohr // inject in password reset 311c08a51cSAndreas Gohr $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 321c08a51cSAndreas Gohr $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 33643f15bdSAndreas Gohr 34643f15bdSAndreas Gohr // inject in login form 351c08a51cSAndreas Gohr $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // old 361c08a51cSAndreas Gohr $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // new 371c08a51cSAndreas Gohr 38643f15bdSAndreas Gohr // check on login 391c08a51cSAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handle_login', []); 40cde3ece1SAndreas Gohr 41cde3ece1SAndreas Gohr // clean up captcha cookies 421c08a51cSAndreas Gohr $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handle_indexer', []); 43969b14c4SAndreas Gohr 44969b14c4SAndreas Gohr $onk = $this->getConf('loginprotect'); 45969b14c4SAndreas Gohr 46969b14c4SAndreas Gohr // log authentication failures 47969b14c4SAndreas Gohr if ((int)$this->getConf('loginprotect') > 1) { 48969b14c4SAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_auth', []); 49969b14c4SAndreas Gohr } 5042a27035SAndreas Gohr } 5142a27035SAndreas Gohr 5242a27035SAndreas Gohr /** 53bd26d35bSAndreas Gohr * Check if the current mode should be handled by CAPTCHA 54bd26d35bSAndreas Gohr * 55f74276b8SAndreas Gohr * Note: checking needs to be done when a form has been submitted, not when the form 56f74276b8SAndreas Gohr * is shown for the first time. Except for the editing process this is not determined 57f74276b8SAndreas Gohr * by $act alone but needs to inspect other input variables. 58f74276b8SAndreas Gohr * 59bd26d35bSAndreas Gohr * @param string $act cleaned action mode 60bd26d35bSAndreas Gohr * @return bool 61bd26d35bSAndreas Gohr */ 621c08a51cSAndreas Gohr protected function needs_checking($act) 631c08a51cSAndreas Gohr { 64bd26d35bSAndreas Gohr global $INPUT; 65bd26d35bSAndreas Gohr 66bd26d35bSAndreas Gohr switch ($act) { 67bd26d35bSAndreas Gohr case 'save': 68bd26d35bSAndreas Gohr return true; 69bd26d35bSAndreas Gohr case 'register': 70f74276b8SAndreas Gohr case 'resendpwd': 71bd26d35bSAndreas Gohr return $INPUT->bool('save'); 72643f15bdSAndreas Gohr case 'login': 73643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 74bd26d35bSAndreas Gohr default: 75bd26d35bSAndreas Gohr return false; 76bd26d35bSAndreas Gohr } 77bd26d35bSAndreas Gohr } 78bd26d35bSAndreas Gohr 79bd26d35bSAndreas Gohr /** 80bd26d35bSAndreas Gohr * Aborts the given mode 81bd26d35bSAndreas Gohr * 82bd26d35bSAndreas Gohr * Aborting depends on the mode. It might unset certain input parameters or simply switch 83bd26d35bSAndreas Gohr * the mode to something else (giving as return which needs to be passed back to the 84bd26d35bSAndreas Gohr * ACTION_ACT_PREPROCESS event) 85bd26d35bSAndreas Gohr * 86bd26d35bSAndreas Gohr * @param string $act cleaned action mode 87bd26d35bSAndreas Gohr * @return string the new mode to use 88bd26d35bSAndreas Gohr */ 891c08a51cSAndreas Gohr protected function abort_action($act) 901c08a51cSAndreas Gohr { 91bd26d35bSAndreas Gohr global $INPUT; 92bd26d35bSAndreas Gohr 93bd26d35bSAndreas Gohr switch ($act) { 94bd26d35bSAndreas Gohr case 'save': 95bd26d35bSAndreas Gohr return 'preview'; 96bd26d35bSAndreas Gohr case 'register': 97f74276b8SAndreas Gohr case 'resendpwd': 98bd26d35bSAndreas Gohr $INPUT->post->set('save', false); 99f74276b8SAndreas Gohr return $act; 100643f15bdSAndreas Gohr case 'login': 101643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 102bd26d35bSAndreas Gohr default: 103bd26d35bSAndreas Gohr return $act; 104bd26d35bSAndreas Gohr } 105bd26d35bSAndreas Gohr } 106bd26d35bSAndreas Gohr 107bd26d35bSAndreas Gohr /** 108969b14c4SAndreas Gohr * Should a login CAPTCHA be used? 109969b14c4SAndreas Gohr * 110969b14c4SAndreas Gohr * @return bool 111969b14c4SAndreas Gohr */ 112969b14c4SAndreas Gohr protected function protectLogin() 113969b14c4SAndreas Gohr { 114969b14c4SAndreas Gohr $config = (int)$this->getConf('loginprotect'); 115969b14c4SAndreas Gohr if ($config < 1) return false; // not wanted 116969b14c4SAndreas Gohr if ($config === 1) return true; // always wanted 117969b14c4SAndreas Gohr $count = (new IpCounter())->get(); 118969b14c4SAndreas Gohr return $count > 2; // only after 3 failed attempts 119969b14c4SAndreas Gohr } 120969b14c4SAndreas Gohr 121969b14c4SAndreas Gohr /** 122643f15bdSAndreas Gohr * Handles CAPTCHA check in login 123643f15bdSAndreas Gohr * 124643f15bdSAndreas Gohr * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them 125643f15bdSAndreas Gohr * in their own event. 126643f15bdSAndreas Gohr * 127643f15bdSAndreas Gohr * @param Doku_Event $event 128643f15bdSAndreas Gohr * @param $param 129643f15bdSAndreas Gohr */ 1301c08a51cSAndreas Gohr public function handle_login(Doku_Event $event, $param) 1311c08a51cSAndreas Gohr { 132643f15bdSAndreas Gohr global $INPUT; 133969b14c4SAndreas Gohr if (!$this->protectLogin()) return; // no protection wanted 134643f15bdSAndreas Gohr if (!$INPUT->bool('u')) return; // this login was not triggered by a form 135643f15bdSAndreas Gohr 136643f15bdSAndreas Gohr // we need to have $ID set for the captcha check 137643f15bdSAndreas Gohr global $ID; 138643f15bdSAndreas Gohr $ID = getID(); 139643f15bdSAndreas Gohr 140643f15bdSAndreas Gohr /** @var helper_plugin_captcha $helper */ 141643f15bdSAndreas Gohr $helper = plugin_load('helper', 'captcha'); 142643f15bdSAndreas Gohr if (!$helper->check()) { 143643f15bdSAndreas Gohr $event->data['silent'] = true; // we have our own message 144643f15bdSAndreas Gohr $event->result = false; // login fail 145643f15bdSAndreas Gohr $event->preventDefault(); 146643f15bdSAndreas Gohr $event->stopPropagation(); 147643f15bdSAndreas Gohr } 148643f15bdSAndreas Gohr } 149643f15bdSAndreas Gohr 150643f15bdSAndreas Gohr /** 151643f15bdSAndreas Gohr * Intercept all actions and check for CAPTCHA first. 15242a27035SAndreas Gohr */ 1531c08a51cSAndreas Gohr public function handle_captcha_input(Doku_Event $event, $param) 1541c08a51cSAndreas Gohr { 155*64382f29SAndreas Gohr global $INPUT; 156*64382f29SAndreas Gohr 1577218f96cSAndreas Gohr $act = act_clean($event->data); 158f74276b8SAndreas Gohr if (!$this->needs_checking($act)) return; 15993f66506SAndreas Gohr 16042a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 161*64382f29SAndreas Gohr if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) { 16242a27035SAndreas Gohr return; 16342a27035SAndreas Gohr } 16442a27035SAndreas Gohr 16577e00bf9SAndreas Gohr // check captcha 1667218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 16777e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 16877e00bf9SAndreas Gohr if (!$helper->check()) { 169bd26d35bSAndreas Gohr $event->data = $this->abort_action($act); 17042a27035SAndreas Gohr } 17142a27035SAndreas Gohr } 17242a27035SAndreas Gohr 17342a27035SAndreas Gohr /** 1741c08a51cSAndreas Gohr * Inject the CAPTCHA in a DokuForm or \dokuwiki\Form\Form 17542a27035SAndreas Gohr */ 1761c08a51cSAndreas Gohr public function handle_form_output(Doku_Event $event, $param) 1771c08a51cSAndreas Gohr { 178*64382f29SAndreas Gohr global $INPUT; 179*64382f29SAndreas Gohr 180c0439b03SAndreas Gohr if ( 181c0439b03SAndreas Gohr ($event->name === 'FORM_LOGIN_OUTPUT' || $event->name === 'HTML_LOGINFORM_OUTPUT') 182c0439b03SAndreas Gohr && 183c0439b03SAndreas Gohr !$this->protectLogin() 184c0439b03SAndreas Gohr ) { 185c0439b03SAndreas Gohr // no login protection wanted 186c0439b03SAndreas Gohr return; 187c0439b03SAndreas Gohr } 18831c8e2bdSAndreas Gohr 1891c08a51cSAndreas Gohr /** @var \dokuwiki\Form\Form|\Doku_Form $form */ 1901c08a51cSAndreas Gohr $form = $event->data; 1911c08a51cSAndreas Gohr 19247afabe6SAndreas Gohr // get position of submit button 1931c08a51cSAndreas Gohr if (is_a($form, \dokuwiki\Form\Form::class)) { 1941c08a51cSAndreas Gohr $pos = $form->findPositionByAttribute('type', 'submit'); 1951c08a51cSAndreas Gohr } else { 1961c08a51cSAndreas Gohr $pos = $form->findElementByAttribute('type', 'submit'); 1971c08a51cSAndreas Gohr } 19847afabe6SAndreas Gohr if (!$pos) return; // no button -> source view mode 19947afabe6SAndreas Gohr 20042a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 201*64382f29SAndreas Gohr if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) { 20242a27035SAndreas Gohr return; 20342a27035SAndreas Gohr } 20442a27035SAndreas Gohr 20577e00bf9SAndreas Gohr // get the CAPTCHA 2067218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 20777e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 20877e00bf9SAndreas Gohr $out = $helper->getHTML(); 20947afabe6SAndreas Gohr 2101c08a51cSAndreas Gohr // insert before the submit button 2111c08a51cSAndreas Gohr if (is_a($form, \dokuwiki\Form\Form::class)) { 2121c08a51cSAndreas Gohr $form->addHTML($out, $pos); 2131c08a51cSAndreas Gohr } else { 2141c08a51cSAndreas Gohr $form->insertElement($pos, $out); 2151c08a51cSAndreas Gohr } 21642a27035SAndreas Gohr } 21742a27035SAndreas Gohr 218cde3ece1SAndreas Gohr /** 219cde3ece1SAndreas Gohr * Clean cookies once per day 220cde3ece1SAndreas Gohr */ 2211c08a51cSAndreas Gohr public function handle_indexer(Doku_Event $event, $param) 2221c08a51cSAndreas Gohr { 223cde3ece1SAndreas Gohr $lastrun = getCacheName('captcha', '.captcha'); 224cde3ece1SAndreas Gohr $last = @filemtime($lastrun); 225cde3ece1SAndreas Gohr if (time() - $last < 24 * 60 * 60) return; 226cde3ece1SAndreas Gohr 227cde3ece1SAndreas Gohr /** @var helper_plugin_captcha $helper */ 228cde3ece1SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 229cde3ece1SAndreas Gohr $helper->_cleanCaptchaCookies(); 2305d59bd09SAndreas Gohr touch($lastrun); 231cde3ece1SAndreas Gohr 232cde3ece1SAndreas Gohr $event->preventDefault(); 233cde3ece1SAndreas Gohr $event->stopPropagation(); 234cde3ece1SAndreas Gohr } 235969b14c4SAndreas Gohr 236969b14c4SAndreas Gohr /** 237969b14c4SAndreas Gohr * Count failed login attempts 238969b14c4SAndreas Gohr */ 239969b14c4SAndreas Gohr public function handle_auth(Doku_Event $event, $param) 240969b14c4SAndreas Gohr { 241969b14c4SAndreas Gohr global $INPUT; 242969b14c4SAndreas Gohr $act = act_clean($event->data); 243969b14c4SAndreas Gohr if ( 244969b14c4SAndreas Gohr $act != 'logout' && 24531c8e2bdSAndreas Gohr $INPUT->str('u') !== '' && 246969b14c4SAndreas Gohr empty($INPUT->server->str('http_credentials')) && 247969b14c4SAndreas Gohr empty($INPUT->server->str('REMOTE_USER')) 248969b14c4SAndreas Gohr ) { 249969b14c4SAndreas Gohr // This is a failed authentication attempt, count it 250969b14c4SAndreas Gohr (new IpCounter())->increment(); 251969b14c4SAndreas Gohr } 252969b14c4SAndreas Gohr 253969b14c4SAndreas Gohr if ( 254969b14c4SAndreas Gohr $act == 'login' && 255969b14c4SAndreas Gohr !empty($INPUT->server->str('REMOTE_USER')) 256969b14c4SAndreas Gohr ) { 257969b14c4SAndreas Gohr // This is a successful login, reset the counter 258969b14c4SAndreas Gohr (new IpCounter())->reset(); 259969b14c4SAndreas Gohr } 260969b14c4SAndreas Gohr } 26142a27035SAndreas Gohr} 26242a27035SAndreas Gohr 263