142a27035SAndreas Gohr<?php 242a27035SAndreas Gohr/** 342a27035SAndreas Gohr * CAPTCHA antispam plugin 442a27035SAndreas Gohr * 542a27035SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 642a27035SAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 742a27035SAndreas Gohr */ 8*1c08a51cSAndreas Gohrclass action_plugin_captcha extends DokuWiki_Action_Plugin 9*1c08a51cSAndreas Gohr{ 1042a27035SAndreas Gohr 1142a27035SAndreas Gohr /** 1242a27035SAndreas Gohr * register the eventhandlers 1342a27035SAndreas Gohr */ 14*1c08a51cSAndreas Gohr public function register(Doku_Event_Handler $controller) 15*1c08a51cSAndreas Gohr { 167218f96cSAndreas Gohr // check CAPTCHA success 17*1c08a51cSAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_captcha_input', []); 1842a27035SAndreas Gohr 197218f96cSAndreas Gohr // inject in edit form 20*1c08a51cSAndreas Gohr $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 21*1c08a51cSAndreas Gohr $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 2242a27035SAndreas Gohr 237218f96cSAndreas Gohr // inject in user registration 24*1c08a51cSAndreas Gohr $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 25*1c08a51cSAndreas Gohr $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 26f74276b8SAndreas Gohr 27f74276b8SAndreas Gohr // inject in password reset 28*1c08a51cSAndreas Gohr $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 29*1c08a51cSAndreas Gohr $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 30643f15bdSAndreas Gohr 31643f15bdSAndreas Gohr if ($this->getConf('loginprotect')) { 32643f15bdSAndreas Gohr // inject in login form 33*1c08a51cSAndreas Gohr $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // old 34*1c08a51cSAndreas Gohr $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // new 35*1c08a51cSAndreas Gohr 36643f15bdSAndreas Gohr // check on login 37*1c08a51cSAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handle_login', []); 38643f15bdSAndreas Gohr } 39cde3ece1SAndreas Gohr 40cde3ece1SAndreas Gohr // clean up captcha cookies 41*1c08a51cSAndreas Gohr $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handle_indexer', []); 4242a27035SAndreas Gohr } 4342a27035SAndreas Gohr 4442a27035SAndreas Gohr /** 45bd26d35bSAndreas Gohr * Check if the current mode should be handled by CAPTCHA 46bd26d35bSAndreas Gohr * 47f74276b8SAndreas Gohr * Note: checking needs to be done when a form has been submitted, not when the form 48f74276b8SAndreas Gohr * is shown for the first time. Except for the editing process this is not determined 49f74276b8SAndreas Gohr * by $act alone but needs to inspect other input variables. 50f74276b8SAndreas Gohr * 51bd26d35bSAndreas Gohr * @param string $act cleaned action mode 52bd26d35bSAndreas Gohr * @return bool 53bd26d35bSAndreas Gohr */ 54*1c08a51cSAndreas Gohr protected function needs_checking($act) 55*1c08a51cSAndreas Gohr { 56bd26d35bSAndreas Gohr global $INPUT; 57bd26d35bSAndreas Gohr 58bd26d35bSAndreas Gohr switch ($act) { 59bd26d35bSAndreas Gohr case 'save': 60bd26d35bSAndreas Gohr return true; 61bd26d35bSAndreas Gohr case 'register': 62f74276b8SAndreas Gohr case 'resendpwd': 63bd26d35bSAndreas Gohr return $INPUT->bool('save'); 64643f15bdSAndreas Gohr case 'login': 65643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 66bd26d35bSAndreas Gohr default: 67bd26d35bSAndreas Gohr return false; 68bd26d35bSAndreas Gohr } 69bd26d35bSAndreas Gohr } 70bd26d35bSAndreas Gohr 71bd26d35bSAndreas Gohr /** 72bd26d35bSAndreas Gohr * Aborts the given mode 73bd26d35bSAndreas Gohr * 74bd26d35bSAndreas Gohr * Aborting depends on the mode. It might unset certain input parameters or simply switch 75bd26d35bSAndreas Gohr * the mode to something else (giving as return which needs to be passed back to the 76bd26d35bSAndreas Gohr * ACTION_ACT_PREPROCESS event) 77bd26d35bSAndreas Gohr * 78bd26d35bSAndreas Gohr * @param string $act cleaned action mode 79bd26d35bSAndreas Gohr * @return string the new mode to use 80bd26d35bSAndreas Gohr */ 81*1c08a51cSAndreas Gohr protected function abort_action($act) 82*1c08a51cSAndreas Gohr { 83bd26d35bSAndreas Gohr global $INPUT; 84bd26d35bSAndreas Gohr 85bd26d35bSAndreas Gohr switch ($act) { 86bd26d35bSAndreas Gohr case 'save': 87bd26d35bSAndreas Gohr return 'preview'; 88bd26d35bSAndreas Gohr case 'register': 89f74276b8SAndreas Gohr case 'resendpwd': 90bd26d35bSAndreas Gohr $INPUT->post->set('save', false); 91f74276b8SAndreas Gohr return $act; 92643f15bdSAndreas Gohr case 'login': 93643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 94bd26d35bSAndreas Gohr default: 95bd26d35bSAndreas Gohr return $act; 96bd26d35bSAndreas Gohr } 97bd26d35bSAndreas Gohr } 98bd26d35bSAndreas Gohr 99bd26d35bSAndreas Gohr /** 100643f15bdSAndreas Gohr * Handles CAPTCHA check in login 101643f15bdSAndreas Gohr * 102643f15bdSAndreas Gohr * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them 103643f15bdSAndreas Gohr * in their own event. 104643f15bdSAndreas Gohr * 105643f15bdSAndreas Gohr * @param Doku_Event $event 106643f15bdSAndreas Gohr * @param $param 107643f15bdSAndreas Gohr */ 108*1c08a51cSAndreas Gohr public function handle_login(Doku_Event $event, $param) 109*1c08a51cSAndreas Gohr { 110643f15bdSAndreas Gohr global $INPUT; 111643f15bdSAndreas Gohr if (!$this->getConf('loginprotect')) return; // no protection wanted 112643f15bdSAndreas Gohr if (!$INPUT->bool('u')) return; // this login was not triggered by a form 113643f15bdSAndreas Gohr 114643f15bdSAndreas Gohr // we need to have $ID set for the captcha check 115643f15bdSAndreas Gohr global $ID; 116643f15bdSAndreas Gohr $ID = getID(); 117643f15bdSAndreas Gohr 118643f15bdSAndreas Gohr /** @var helper_plugin_captcha $helper */ 119643f15bdSAndreas Gohr $helper = plugin_load('helper', 'captcha'); 120643f15bdSAndreas Gohr if (!$helper->check()) { 121643f15bdSAndreas Gohr $event->data['silent'] = true; // we have our own message 122643f15bdSAndreas Gohr $event->result = false; // login fail 123643f15bdSAndreas Gohr $event->preventDefault(); 124643f15bdSAndreas Gohr $event->stopPropagation(); 125643f15bdSAndreas Gohr } 126643f15bdSAndreas Gohr } 127643f15bdSAndreas Gohr 128643f15bdSAndreas Gohr /** 129643f15bdSAndreas Gohr * Intercept all actions and check for CAPTCHA first. 13042a27035SAndreas Gohr */ 131*1c08a51cSAndreas Gohr public function handle_captcha_input(Doku_Event $event, $param) 132*1c08a51cSAndreas Gohr { 1337218f96cSAndreas Gohr $act = act_clean($event->data); 134f74276b8SAndreas Gohr if (!$this->needs_checking($act)) return; 13593f66506SAndreas Gohr 13642a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 13742a27035SAndreas Gohr if (!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) { 13842a27035SAndreas Gohr return; 13942a27035SAndreas Gohr } 14042a27035SAndreas Gohr 14177e00bf9SAndreas Gohr // check captcha 1427218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 14377e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 14477e00bf9SAndreas Gohr if (!$helper->check()) { 145bd26d35bSAndreas Gohr $event->data = $this->abort_action($act); 14642a27035SAndreas Gohr } 14742a27035SAndreas Gohr } 14842a27035SAndreas Gohr 14942a27035SAndreas Gohr /** 150*1c08a51cSAndreas Gohr * Inject the CAPTCHA in a DokuForm or \dokuwiki\Form\Form 15142a27035SAndreas Gohr */ 152*1c08a51cSAndreas Gohr public function handle_form_output(Doku_Event $event, $param) 153*1c08a51cSAndreas Gohr { 154*1c08a51cSAndreas Gohr /** @var \dokuwiki\Form\Form|\Doku_Form $form */ 155*1c08a51cSAndreas Gohr $form = $event->data; 156*1c08a51cSAndreas Gohr 15747afabe6SAndreas Gohr // get position of submit button 158*1c08a51cSAndreas Gohr if (is_a($form, \dokuwiki\Form\Form::class)) { 159*1c08a51cSAndreas Gohr $pos = $form->findPositionByAttribute('type', 'submit'); 160*1c08a51cSAndreas Gohr } else { 161*1c08a51cSAndreas Gohr $pos = $form->findElementByAttribute('type', 'submit'); 162*1c08a51cSAndreas Gohr } 16347afabe6SAndreas Gohr if (!$pos) return; // no button -> source view mode 16447afabe6SAndreas Gohr 16542a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 16642a27035SAndreas Gohr if (!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) { 16742a27035SAndreas Gohr return; 16842a27035SAndreas Gohr } 16942a27035SAndreas Gohr 17077e00bf9SAndreas Gohr // get the CAPTCHA 1717218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 17277e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 17377e00bf9SAndreas Gohr $out = $helper->getHTML(); 17447afabe6SAndreas Gohr 175*1c08a51cSAndreas Gohr // insert before the submit button 176*1c08a51cSAndreas Gohr if (is_a($form, \dokuwiki\Form\Form::class)) { 177*1c08a51cSAndreas Gohr $form->addHTML($out, $pos); 178*1c08a51cSAndreas Gohr } else { 179*1c08a51cSAndreas Gohr $form->insertElement($pos, $out); 180*1c08a51cSAndreas Gohr } 18142a27035SAndreas Gohr } 18242a27035SAndreas Gohr 183cde3ece1SAndreas Gohr /** 184cde3ece1SAndreas Gohr * Clean cookies once per day 185cde3ece1SAndreas Gohr */ 186*1c08a51cSAndreas Gohr public function handle_indexer(Doku_Event $event, $param) 187*1c08a51cSAndreas Gohr { 188cde3ece1SAndreas Gohr $lastrun = getCacheName('captcha', '.captcha'); 189cde3ece1SAndreas Gohr $last = @filemtime($lastrun); 190cde3ece1SAndreas Gohr if (time() - $last < 24 * 60 * 60) return; 191cde3ece1SAndreas Gohr 192cde3ece1SAndreas Gohr /** @var helper_plugin_captcha $helper */ 193cde3ece1SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 194cde3ece1SAndreas Gohr $helper->_cleanCaptchaCookies(); 1955d59bd09SAndreas Gohr touch($lastrun); 196cde3ece1SAndreas Gohr 197cde3ece1SAndreas Gohr $event->preventDefault(); 198cde3ece1SAndreas Gohr $event->stopPropagation(); 199cde3ece1SAndreas Gohr } 20042a27035SAndreas Gohr} 20142a27035SAndreas Gohr 202