142a27035SAndreas Gohr<?php 2*18622736SAndreas Gohr 342a27035SAndreas Gohr/** 442a27035SAndreas Gohr * CAPTCHA antispam plugin 542a27035SAndreas Gohr * 642a27035SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 742a27035SAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 842a27035SAndreas Gohr */ 91c08a51cSAndreas Gohrclass action_plugin_captcha extends DokuWiki_Action_Plugin 101c08a51cSAndreas Gohr{ 1142a27035SAndreas Gohr 1242a27035SAndreas Gohr /** 1342a27035SAndreas Gohr * register the eventhandlers 1442a27035SAndreas Gohr */ 151c08a51cSAndreas Gohr public function register(Doku_Event_Handler $controller) 161c08a51cSAndreas Gohr { 177218f96cSAndreas Gohr // check CAPTCHA success 181c08a51cSAndreas Gohr $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_captcha_input', []); 1942a27035SAndreas Gohr 207218f96cSAndreas Gohr // inject in edit form 211c08a51cSAndreas Gohr $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 221c08a51cSAndreas Gohr $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 2342a27035SAndreas Gohr 247218f96cSAndreas Gohr // inject in user registration 251c08a51cSAndreas Gohr $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 261c08a51cSAndreas Gohr $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 27f74276b8SAndreas Gohr 28f74276b8SAndreas Gohr // inject in password reset 291c08a51cSAndreas Gohr $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old 301c08a51cSAndreas Gohr $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new 31643f15bdSAndreas Gohr 32643f15bdSAndreas Gohr if ($this->getConf('loginprotect')) { 33643f15bdSAndreas Gohr // inject in login form 341c08a51cSAndreas Gohr $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // old 351c08a51cSAndreas Gohr $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // new 361c08a51cSAndreas Gohr 37643f15bdSAndreas Gohr // check on login 381c08a51cSAndreas Gohr $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handle_login', []); 39643f15bdSAndreas Gohr } 40cde3ece1SAndreas Gohr 41cde3ece1SAndreas Gohr // clean up captcha cookies 421c08a51cSAndreas Gohr $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handle_indexer', []); 4342a27035SAndreas Gohr } 4442a27035SAndreas Gohr 4542a27035SAndreas Gohr /** 46bd26d35bSAndreas Gohr * Check if the current mode should be handled by CAPTCHA 47bd26d35bSAndreas Gohr * 48f74276b8SAndreas Gohr * Note: checking needs to be done when a form has been submitted, not when the form 49f74276b8SAndreas Gohr * is shown for the first time. Except for the editing process this is not determined 50f74276b8SAndreas Gohr * by $act alone but needs to inspect other input variables. 51f74276b8SAndreas Gohr * 52bd26d35bSAndreas Gohr * @param string $act cleaned action mode 53bd26d35bSAndreas Gohr * @return bool 54bd26d35bSAndreas Gohr */ 551c08a51cSAndreas Gohr protected function needs_checking($act) 561c08a51cSAndreas Gohr { 57bd26d35bSAndreas Gohr global $INPUT; 58bd26d35bSAndreas Gohr 59bd26d35bSAndreas Gohr switch ($act) { 60bd26d35bSAndreas Gohr case 'save': 61bd26d35bSAndreas Gohr return true; 62bd26d35bSAndreas Gohr case 'register': 63f74276b8SAndreas Gohr case 'resendpwd': 64bd26d35bSAndreas Gohr return $INPUT->bool('save'); 65643f15bdSAndreas Gohr case 'login': 66643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 67bd26d35bSAndreas Gohr default: 68bd26d35bSAndreas Gohr return false; 69bd26d35bSAndreas Gohr } 70bd26d35bSAndreas Gohr } 71bd26d35bSAndreas Gohr 72bd26d35bSAndreas Gohr /** 73bd26d35bSAndreas Gohr * Aborts the given mode 74bd26d35bSAndreas Gohr * 75bd26d35bSAndreas Gohr * Aborting depends on the mode. It might unset certain input parameters or simply switch 76bd26d35bSAndreas Gohr * the mode to something else (giving as return which needs to be passed back to the 77bd26d35bSAndreas Gohr * ACTION_ACT_PREPROCESS event) 78bd26d35bSAndreas Gohr * 79bd26d35bSAndreas Gohr * @param string $act cleaned action mode 80bd26d35bSAndreas Gohr * @return string the new mode to use 81bd26d35bSAndreas Gohr */ 821c08a51cSAndreas Gohr protected function abort_action($act) 831c08a51cSAndreas Gohr { 84bd26d35bSAndreas Gohr global $INPUT; 85bd26d35bSAndreas Gohr 86bd26d35bSAndreas Gohr switch ($act) { 87bd26d35bSAndreas Gohr case 'save': 88bd26d35bSAndreas Gohr return 'preview'; 89bd26d35bSAndreas Gohr case 'register': 90f74276b8SAndreas Gohr case 'resendpwd': 91bd26d35bSAndreas Gohr $INPUT->post->set('save', false); 92f74276b8SAndreas Gohr return $act; 93643f15bdSAndreas Gohr case 'login': 94643f15bdSAndreas Gohr // we do not handle this here, but in handle_login() 95bd26d35bSAndreas Gohr default: 96bd26d35bSAndreas Gohr return $act; 97bd26d35bSAndreas Gohr } 98bd26d35bSAndreas Gohr } 99bd26d35bSAndreas Gohr 100bd26d35bSAndreas Gohr /** 101643f15bdSAndreas Gohr * Handles CAPTCHA check in login 102643f15bdSAndreas Gohr * 103643f15bdSAndreas Gohr * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them 104643f15bdSAndreas Gohr * in their own event. 105643f15bdSAndreas Gohr * 106643f15bdSAndreas Gohr * @param Doku_Event $event 107643f15bdSAndreas Gohr * @param $param 108643f15bdSAndreas Gohr */ 1091c08a51cSAndreas Gohr public function handle_login(Doku_Event $event, $param) 1101c08a51cSAndreas Gohr { 111643f15bdSAndreas Gohr global $INPUT; 112643f15bdSAndreas Gohr if (!$this->getConf('loginprotect')) return; // no protection wanted 113643f15bdSAndreas Gohr if (!$INPUT->bool('u')) return; // this login was not triggered by a form 114643f15bdSAndreas Gohr 115643f15bdSAndreas Gohr // we need to have $ID set for the captcha check 116643f15bdSAndreas Gohr global $ID; 117643f15bdSAndreas Gohr $ID = getID(); 118643f15bdSAndreas Gohr 119643f15bdSAndreas Gohr /** @var helper_plugin_captcha $helper */ 120643f15bdSAndreas Gohr $helper = plugin_load('helper', 'captcha'); 121643f15bdSAndreas Gohr if (!$helper->check()) { 122643f15bdSAndreas Gohr $event->data['silent'] = true; // we have our own message 123643f15bdSAndreas Gohr $event->result = false; // login fail 124643f15bdSAndreas Gohr $event->preventDefault(); 125643f15bdSAndreas Gohr $event->stopPropagation(); 126643f15bdSAndreas Gohr } 127643f15bdSAndreas Gohr } 128643f15bdSAndreas Gohr 129643f15bdSAndreas Gohr /** 130643f15bdSAndreas Gohr * Intercept all actions and check for CAPTCHA first. 13142a27035SAndreas Gohr */ 1321c08a51cSAndreas Gohr public function handle_captcha_input(Doku_Event $event, $param) 1331c08a51cSAndreas Gohr { 1347218f96cSAndreas Gohr $act = act_clean($event->data); 135f74276b8SAndreas Gohr if (!$this->needs_checking($act)) return; 13693f66506SAndreas Gohr 13742a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 13842a27035SAndreas Gohr if (!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) { 13942a27035SAndreas Gohr return; 14042a27035SAndreas Gohr } 14142a27035SAndreas Gohr 14277e00bf9SAndreas Gohr // check captcha 1437218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 14477e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 14577e00bf9SAndreas Gohr if (!$helper->check()) { 146bd26d35bSAndreas Gohr $event->data = $this->abort_action($act); 14742a27035SAndreas Gohr } 14842a27035SAndreas Gohr } 14942a27035SAndreas Gohr 15042a27035SAndreas Gohr /** 1511c08a51cSAndreas Gohr * Inject the CAPTCHA in a DokuForm or \dokuwiki\Form\Form 15242a27035SAndreas Gohr */ 1531c08a51cSAndreas Gohr public function handle_form_output(Doku_Event $event, $param) 1541c08a51cSAndreas Gohr { 1551c08a51cSAndreas Gohr /** @var \dokuwiki\Form\Form|\Doku_Form $form */ 1561c08a51cSAndreas Gohr $form = $event->data; 1571c08a51cSAndreas Gohr 15847afabe6SAndreas Gohr // get position of submit button 1591c08a51cSAndreas Gohr if (is_a($form, \dokuwiki\Form\Form::class)) { 1601c08a51cSAndreas Gohr $pos = $form->findPositionByAttribute('type', 'submit'); 1611c08a51cSAndreas Gohr } else { 1621c08a51cSAndreas Gohr $pos = $form->findElementByAttribute('type', 'submit'); 1631c08a51cSAndreas Gohr } 16447afabe6SAndreas Gohr if (!$pos) return; // no button -> source view mode 16547afabe6SAndreas Gohr 16642a27035SAndreas Gohr // do nothing if logged in user and no CAPTCHA required 16742a27035SAndreas Gohr if (!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) { 16842a27035SAndreas Gohr return; 16942a27035SAndreas Gohr } 17042a27035SAndreas Gohr 17177e00bf9SAndreas Gohr // get the CAPTCHA 1727218f96cSAndreas Gohr /** @var helper_plugin_captcha $helper */ 17377e00bf9SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 17477e00bf9SAndreas Gohr $out = $helper->getHTML(); 17547afabe6SAndreas Gohr 1761c08a51cSAndreas Gohr // insert before the submit button 1771c08a51cSAndreas Gohr if (is_a($form, \dokuwiki\Form\Form::class)) { 1781c08a51cSAndreas Gohr $form->addHTML($out, $pos); 1791c08a51cSAndreas Gohr } else { 1801c08a51cSAndreas Gohr $form->insertElement($pos, $out); 1811c08a51cSAndreas Gohr } 18242a27035SAndreas Gohr } 18342a27035SAndreas Gohr 184cde3ece1SAndreas Gohr /** 185cde3ece1SAndreas Gohr * Clean cookies once per day 186cde3ece1SAndreas Gohr */ 1871c08a51cSAndreas Gohr public function handle_indexer(Doku_Event $event, $param) 1881c08a51cSAndreas Gohr { 189cde3ece1SAndreas Gohr $lastrun = getCacheName('captcha', '.captcha'); 190cde3ece1SAndreas Gohr $last = @filemtime($lastrun); 191cde3ece1SAndreas Gohr if (time() - $last < 24 * 60 * 60) return; 192cde3ece1SAndreas Gohr 193cde3ece1SAndreas Gohr /** @var helper_plugin_captcha $helper */ 194cde3ece1SAndreas Gohr $helper = plugin_load('helper', 'captcha'); 195cde3ece1SAndreas Gohr $helper->_cleanCaptchaCookies(); 1965d59bd09SAndreas Gohr touch($lastrun); 197cde3ece1SAndreas Gohr 198cde3ece1SAndreas Gohr $event->preventDefault(); 199cde3ece1SAndreas Gohr $event->stopPropagation(); 200cde3ece1SAndreas Gohr } 20142a27035SAndreas Gohr} 20242a27035SAndreas Gohr 203