16980370bSSascha Leib<?php 26980370bSSascha Leib 36980370bSSascha Leibuse dokuwiki\Extension\EventHandler; 46980370bSSascha Leibuse dokuwiki\Extension\Event; 55fbe88f7SSascha Leibuse dokuwiki\Logger; 66980370bSSascha Leib 76980370bSSascha Leib/** 87bd08c30SSascha Leib * Action Component for the Bot Monitoring Plugin 96980370bSSascha Leib * 106980370bSSascha Leib * @license GPL 3 (http://www.gnu.org/licenses/gpl.html) 116980370bSSascha Leib * @author Sascha Leib <sascha.leib(at)kolmio.com> 126980370bSSascha Leib */ 136980370bSSascha Leib 147bd08c30SSascha Leibclass action_plugin_botmon extends DokuWiki_Action_Plugin { 156980370bSSascha Leib 166980370bSSascha Leib /** 176980370bSSascha Leib * Registers a callback functions 186980370bSSascha Leib * 196980370bSSascha Leib * @param EventHandler $controller DokuWiki's event controller object 206980370bSSascha Leib * @return void 216980370bSSascha Leib */ 226980370bSSascha Leib public function register(EventHandler $controller) { 235f2c1759SSascha Leib 24e56d7b71SSascha Leib global $ACT; 25e56d7b71SSascha Leib 265f2c1759SSascha Leib // insert header data into the page: 27e56d7b71SSascha Leib if ($ACT == 'show') { 286980370bSSascha Leib $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'insertHeader'); 29e56d7b71SSascha Leib } else if ($ACT == 'admin' && isset($_REQUEST['page']) && $_REQUEST['page'] == 'botmon') { 30e56d7b71SSascha Leib $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'insertAdminHeader'); 31e56d7b71SSascha Leib } 325f2c1759SSascha Leib 33*f5f4ca13SSascha Leib // Override the page rendering, if a captcha needs to be displayed: 34*f5f4ca13SSascha Leib if ($ACT !== 'admin') { 35*f5f4ca13SSascha Leib $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'showCaptcha'); 36*f5f4ca13SSascha Leib } 37*f5f4ca13SSascha Leib 385f2c1759SSascha Leib // write to the log after the page content was displayed: 395f2c1759SSascha Leib $controller->register_hook('TPL_CONTENT_DISPLAY', 'AFTER', $this, 'writeServerLog'); 405f2c1759SSascha Leib 416980370bSSascha Leib } 426980370bSSascha Leib 43b148c85eSSascha Leib /* session information */ 44f6a7ebc1SSascha Leib private $sessionId = null; 45f6a7ebc1SSascha Leib private $sessionType = ''; 46b148c85eSSascha Leib 476980370bSSascha Leib /** 486980370bSSascha Leib * Inserts tracking code to the page header 49e56d7b71SSascha Leib * (only called on 'show' actions) 506980370bSSascha Leib * 516980370bSSascha Leib * @param Event $event event object by reference 526980370bSSascha Leib * @return void 536980370bSSascha Leib */ 546980370bSSascha Leib public function insertHeader(Event $event, $param) { 556980370bSSascha Leib 566980370bSSascha Leib global $INFO; 576980370bSSascha Leib 58b148c85eSSascha Leib // populate the session id and type: 59b148c85eSSascha Leib $this->getSessionInfo(); 60b148c85eSSascha Leib 616980370bSSascha Leib // is there a user logged in? 625f2c1759SSascha Leib $username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) ? $INFO['userinfo']['name'] : ''); 636980370bSSascha Leib 646980370bSSascha Leib // build the tracker code: 65e56d7b71SSascha Leib $code = "document._botmon = {'t0': Date.now(), 'session': '" . json_encode($this->sessionId) . "'};" . NL; 666980370bSSascha Leib if ($username) { 67e56d7b71SSascha Leib $code .= DOKU_TAB . DOKU_TAB . 'document._botmon.user = "' . $username . '";'. NL; 686980370bSSascha Leib } 696980370bSSascha Leib 705f2c1759SSascha Leib // add the deferred script loader:: 71e56d7b71SSascha Leib $code .= DOKU_TAB . DOKU_TAB . "addEventListener('DOMContentLoaded', function(){" . NL; 72e56d7b71SSascha Leib $code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "const e=document.createElement('script');" . NL; 73e56d7b71SSascha Leib $code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "e.async=true;e.defer=true;" . NL; 74e56d7b71SSascha Leib $code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "e.src='".DOKU_BASE."lib/plugins/botmon/client.js';" . NL; 75e56d7b71SSascha Leib $code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(e);" . NL; 76e56d7b71SSascha Leib $code .= DOKU_TAB . DOKU_TAB . "});"; 776980370bSSascha Leib 785f2c1759SSascha Leib $event->data['script'][] = ['_data' => $code]; 79451abfadSSascha Leib } 80451abfadSSascha Leib 81451abfadSSascha Leib /** 82e56d7b71SSascha Leib * Inserts tracking code to the page header 83e56d7b71SSascha Leib * (only called on 'show' actions) 84e56d7b71SSascha Leib * 85e56d7b71SSascha Leib * @param Event $event event object by reference 86e56d7b71SSascha Leib * @return void 87e56d7b71SSascha Leib */ 88e56d7b71SSascha Leib public function insertAdminHeader(Event $event, $param) { 89e56d7b71SSascha Leib 90e56d7b71SSascha Leib $event->data['link'][] = ['rel' => 'stylesheet', 'href' => DOKU_BASE.'lib/plugins/botmon/admin.css', 'defer' => 'defer']; 910edf1a56SSascha Leib $event->data['script'][] = ['src' => DOKU_BASE.'lib/plugins/botmon/admin.js', 'defer' => 'defer', '_data' => '']; 92e56d7b71SSascha Leib } 93e56d7b71SSascha Leib 94e56d7b71SSascha Leib 95e56d7b71SSascha Leib /** 96451abfadSSascha Leib * Writes data to the server log. 97451abfadSSascha Leib * 98451abfadSSascha Leib * @return void 99451abfadSSascha Leib */ 1005f2c1759SSascha Leib public function writeServerLog(Event $event, $param) { 101451abfadSSascha Leib 102451abfadSSascha Leib global $conf; 103451abfadSSascha Leib global $INFO; 104091b5998SSascha Leib 1055f2c1759SSascha Leib // is there a user logged in? 1065f2c1759SSascha Leib $username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) 1075f2c1759SSascha Leib ? $INFO['userinfo']['name'] : ''); 1085f2c1759SSascha Leib 109b2e3bd8bSSascha Leib // clean the page ID 110b2e3bd8bSSascha Leib $pageId = preg_replace('/[\x00-\x1F]/', "\u{FFFD}", $INFO['id'] ?? ''); 111b2e3bd8bSSascha Leib 112451abfadSSascha Leib // create the log array: 113cf9f7fe8SSascha Leib $logArr = Array( 114*f5f4ca13SSascha Leib $_SERVER['REMOTE_ADDR'], /* remote IP */ 115b2e3bd8bSSascha Leib $pageId, /* page ID */ 116b148c85eSSascha Leib $this->sessionId, /* Session ID */ 117b148c85eSSascha Leib $this->sessionType, /* session ID type */ 1185f2c1759SSascha Leib $username, /* user name */ 1192f2bc93aSSascha Leib $_SERVER['HTTP_USER_AGENT'] ?? '', /* User agent */ 120451abfadSSascha Leib $_SERVER['HTTP_REFERER'] ?? '', /* HTTP Referrer */ 121451abfadSSascha Leib substr($conf['lang'],0,2), /* page language */ 122a93de874SSascha Leib implode(',', array_unique(array_map( function($it) { return substr(trim($it),0,2); }, explode(',',trim($_SERVER['HTTP_ACCEPT_LANGUAGE'], " \t;,*"))))), /* accepted client languages */ 1235f2c1759SSascha Leib $this->getCountryCode() /* GeoIP country code */ 124cf9f7fe8SSascha Leib ); 125cf9f7fe8SSascha Leib 126cf9f7fe8SSascha Leib //* create the log line */ 1274cddc661SSascha Leib $filename = __DIR__ .'/logs/' . gmdate('Y-m-d') . '.srv.txt'; /* use GMT date for filename */ 128cf9f7fe8SSascha Leib $logline = gmdate('Y-m-d H:i:s'); /* use GMT time for log entries */ 129cf9f7fe8SSascha Leib foreach ($logArr as $tab) { 130cf9f7fe8SSascha Leib $logline .= "\t" . $tab; 131cf9f7fe8SSascha Leib }; 132cf9f7fe8SSascha Leib 133cf9f7fe8SSascha Leib /* write the log line to the file */ 134cf9f7fe8SSascha Leib $logfile = fopen($filename, 'a'); 135cf9f7fe8SSascha Leib if (!$logfile) die(); 136cf9f7fe8SSascha Leib if (fwrite($logfile, $logline . "\n") === false) { 137cf9f7fe8SSascha Leib fclose($logfile); 138cf9f7fe8SSascha Leib die(); 1396980370bSSascha Leib } 140cf9f7fe8SSascha Leib 141cf9f7fe8SSascha Leib /* Done */ 142cf9f7fe8SSascha Leib fclose($logfile); 143cf9f7fe8SSascha Leib } 144b148c85eSSascha Leib 1455f2c1759SSascha Leib private function getCountryCode() { 1465f2c1759SSascha Leib 147*f5f4ca13SSascha Leib $country = ( $_SERVER['REMOTE_ADDR'] == '127.0.0.1' ? 'local' : 'ZZ' ); // default if no geoip is available! 1485f2c1759SSascha Leib 1495f2c1759SSascha Leib $lib = $this->getConf('geoiplib'); /* which library to use? (can only be phpgeoip or disabled) */ 1505f2c1759SSascha Leib 1515f2c1759SSascha Leib try { 1525f2c1759SSascha Leib 1535f2c1759SSascha Leib // use GeoIP module? 1545f2c1759SSascha Leib if ($lib == 'phpgeoip' && extension_loaded('geoip') && geoip_db_avail(GEOIP_COUNTRY_EDITION)) { // Use PHP GeoIP module 1555f2c1759SSascha Leib $result = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']); 1565f2c1759SSascha Leib $country = ($result ? $result : $country); 1575f2c1759SSascha Leib } 1585f2c1759SSascha Leib } catch (Exception $e) { 1595f2c1759SSascha Leib Logger::error('BotMon Plugin: GeoIP Error', $e->getMessage()); 1605f2c1759SSascha Leib } 1615f2c1759SSascha Leib 1625f2c1759SSascha Leib return $country; 1635f2c1759SSascha Leib } 1645f2c1759SSascha Leib 165b148c85eSSascha Leib private function getSessionInfo() { 166b148c85eSSascha Leib 167b148c85eSSascha Leib // what is the session identifier? 168b148c85eSSascha Leib if (isset($_SESSION)) { 169b148c85eSSascha Leib $sesKeys = array_keys($_SESSION); /* DokuWiki Session ID preferred */ 170b148c85eSSascha Leib foreach ($sesKeys as $key) { 171b148c85eSSascha Leib if (substr($key, 0, 2) == 'DW') { 172b148c85eSSascha Leib $this->sessionId = $key; 173b148c85eSSascha Leib $this->sessionType = 'dw'; 174b148c85eSSascha Leib return; 175b148c85eSSascha Leib } 176b148c85eSSascha Leib } 177b148c85eSSascha Leib } 178f6a7ebc1SSascha Leib if (!$this->sessionId) { /* no DokuWiki Session ID, try PHP session ID */ 179b148c85eSSascha Leib $this->sessionId = session_id(); 180b148c85eSSascha Leib $this->sessionType = 'php'; 181b148c85eSSascha Leib } 182*f5f4ca13SSascha Leib if (!$this->sessionId) { /* no PHP session ID, try IP address */ 183*f5f4ca13SSascha Leib $this->sessionId = $_SERVER['REMOTE_ADDR']; 184b148c85eSSascha Leib $this->sessionType = 'ip'; 185b148c85eSSascha Leib } 186f6a7ebc1SSascha Leib if (!$this->sessionId) { /* if everything else fails, just us a random ID */ 187b148c85eSSascha Leib $this->sessionId = rand(1000000, 9999999); 188b148c85eSSascha Leib $this->sessionType = 'rand'; 189b148c85eSSascha Leib } 190b148c85eSSascha Leib } 191*f5f4ca13SSascha Leib 192*f5f4ca13SSascha Leib public function showCaptcha(Event $event) { 193*f5f4ca13SSascha Leib 194*f5f4ca13SSascha Leib if ($this->getConf('useCaptcha') && $this->checkCaptchaCookie()) { 195*f5f4ca13SSascha Leib 196*f5f4ca13SSascha Leib $event->preventDefault(); // don't show normal content 197*f5f4ca13SSascha Leib $this->insertDadaFiller(); // show dada filler instead! 198*f5f4ca13SSascha Leib $this->insertCaptchaLoader(); // and load the captcha 199*f5f4ca13SSascha Leib 200*f5f4ca13SSascha Leib } else { 201*f5f4ca13SSascha Leib echo '<p>Normal page.</p>'; 202*f5f4ca13SSascha Leib } 203*f5f4ca13SSascha Leib } 204*f5f4ca13SSascha Leib 205*f5f4ca13SSascha Leib private function checkCaptchaCookie() { 206*f5f4ca13SSascha Leib 207*f5f4ca13SSascha Leib $cookieVal = isset($_COOKIE['_c_']) ? $_COOKIE['_c_'] : ''; 208*f5f4ca13SSascha Leib $seed = $this->getConf('captchaSeed'); 209*f5f4ca13SSascha Leib 210*f5f4ca13SSascha Leib return ($cookieVal == $seed ? 0 : 1); // #TODO: encrypt with other data 211*f5f4ca13SSascha Leib } 212*f5f4ca13SSascha Leib 213*f5f4ca13SSascha Leib private function insertCaptchaLoader() { 214*f5f4ca13SSascha Leib 215*f5f4ca13SSascha Leib } 216*f5f4ca13SSascha Leib 217*f5f4ca13SSascha Leib private function insertDadaFiller() { 218*f5f4ca13SSascha Leib // #TODO: make a dada filler 219*f5f4ca13SSascha Leib 220*f5f4ca13SSascha Leib echo '<h1>'; tpl_pagetitle(); echo "</h1>\n"; 221*f5f4ca13SSascha Leib 222*f5f4ca13SSascha Leib echo '<script> alert("Hello world!"); </script>'; 223*f5f4ca13SSascha Leib 224*f5f4ca13SSascha Leib echo "<p>Placeholder text while the captcha is being displayed.</p>\n"; 225*f5f4ca13SSascha Leib 226*f5f4ca13SSascha Leib 227*f5f4ca13SSascha Leib } 228*f5f4ca13SSascha Leib 2296980370bSSascha Leib}