xref: /plugin/botmon/action.php (revision cb91697912a3c2642b72c3d0bc9fa6758b1e5bb4)
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
1619b69b64SSascha Leib	public function __construct() {
1719b69b64SSascha Leib
1819b69b64SSascha Leib		// determine if a captcha should be loaded:
19ad279a21SSascha Leib		$this->showCaptcha = 'Z'; // Captcha unknown
2019b69b64SSascha Leib
2119b69b64SSascha Leib		$useCaptcha = $this->getConf('useCaptcha'); // should we show a captcha?
2219b69b64SSascha Leib
2319b69b64SSascha Leib		if ($useCaptcha !== 'disabled') {
2419b69b64SSascha Leib			if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
2519b69b64SSascha Leib				$this->showCaptcha = 'H'; // Method is HEAD, no need for captcha
2619b69b64SSascha Leib			} elseif ($this->captchaWhitelisted()) {
2719b69b64SSascha Leib				$this->showCaptcha = 'W'; // IP is whitelisted, no captcha
2819b69b64SSascha Leib			} elseif ($this->hasCaptchaCookie()) {
2919b69b64SSascha Leib				$this->showCaptcha = 'N'; // No, user already has a cookie, don't show the captcha
3019b69b64SSascha Leib			} else {
3119b69b64SSascha Leib				$this->showCaptcha = 'Y'; // Yes, show the captcha
3219b69b64SSascha Leib			}
3319b69b64SSascha Leib		}
3419b69b64SSascha Leib	}
3519b69b64SSascha Leib
366980370bSSascha Leib	/**
376980370bSSascha Leib	 * Registers a callback functions
386980370bSSascha Leib	 *
396980370bSSascha Leib	 * @param EventHandler $controller DokuWiki's event controller object
406980370bSSascha Leib	 * @return void
416980370bSSascha Leib	 */
426980370bSSascha Leib	public function register(EventHandler $controller) {
435f2c1759SSascha Leib
44e56d7b71SSascha Leib		global $ACT;
45e56d7b71SSascha Leib
46447b8b4fSSascha Leib		// populate the session id and type:
47447b8b4fSSascha Leib		$this->setSessionInfo();
48d49ab213SSascha Leib
495f2c1759SSascha Leib		// insert header data into the page:
50d49ab213SSascha Leib		if ($ACT == 'show' || $ACT == 'edit' || $ACT == 'media') {
516980370bSSascha Leib			$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'insertHeader');
52d49ab213SSascha Leib
53d49ab213SSascha Leib			// Override the page rendering, if a captcha needs to be displayed:
54393de67cSSascha Leib			$controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'insertCaptchaCode');
55d49ab213SSascha Leib
56e56d7b71SSascha Leib		} else if ($ACT == 'admin' && isset($_REQUEST['page']) && $_REQUEST['page'] == 'botmon') {
57e56d7b71SSascha Leib			$controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'insertAdminHeader');
58e56d7b71SSascha Leib		}
595f2c1759SSascha Leib
60d49ab213SSascha Leib		// also show a captcha before the image preview
61d49ab213SSascha Leib		$controller->register_hook('TPL_IMG_DISPLAY', 'BEFORE', $this, 'showImageCaptcha');
62f5f4ca13SSascha Leib
635f2c1759SSascha Leib		// write to the log after the page content was displayed:
645f2c1759SSascha Leib		$controller->register_hook('TPL_CONTENT_DISPLAY', 'AFTER', $this, 'writeServerLog');
655f2c1759SSascha Leib
666980370bSSascha Leib	}
676980370bSSascha Leib
68b148c85eSSascha Leib	/* session information */
69f6a7ebc1SSascha Leib	private $sessionId = null;
70f6a7ebc1SSascha Leib	private $sessionType = '';
71393de67cSSascha Leib	private $showCaptcha = 'X';
72b148c85eSSascha Leib
736980370bSSascha Leib	/**
746980370bSSascha Leib	 * Inserts tracking code to the page header
75e56d7b71SSascha Leib	 * (only called on 'show' actions)
766980370bSSascha Leib	 *
776980370bSSascha Leib	 * @param Event $event event object by reference
786980370bSSascha Leib	 * @return void
796980370bSSascha Leib	 */
806980370bSSascha Leib	public function insertHeader(Event $event, $param) {
816980370bSSascha Leib
826980370bSSascha Leib		global $INFO;
836980370bSSascha Leib
84b148c85eSSascha Leib
856980370bSSascha Leib		// build the tracker code:
86d49ab213SSascha Leib		$code = $this->getBMHeader();
876980370bSSascha Leib
885f2c1759SSascha Leib		// add the deferred script loader::
89e56d7b71SSascha Leib		$code .= DOKU_TAB . DOKU_TAB . "addEventListener('DOMContentLoaded', function(){" . NL;
90e56d7b71SSascha Leib		$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "const e=document.createElement('script');" . NL;
91e56d7b71SSascha Leib		$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "e.async=true;e.defer=true;" . NL;
92e56d7b71SSascha Leib		$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "e.src='".DOKU_BASE."lib/plugins/botmon/client.js';" . NL;
93e56d7b71SSascha Leib		$code .= DOKU_TAB . DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(e);" . NL;
94e56d7b71SSascha Leib		$code .= DOKU_TAB . DOKU_TAB . "});";
955f2c1759SSascha Leib		$event->data['script'][] = ['_data' => $code];
96451abfadSSascha Leib	}
97451abfadSSascha Leib
98d49ab213SSascha Leib	/* create the BM object code for insertion into a script element: */
99d49ab213SSascha Leib	private function getBMHeader() {
100d49ab213SSascha Leib
101d49ab213SSascha Leib		// build the tracker code:
102d49ab213SSascha Leib		$code = DOKU_TAB . DOKU_TAB . "document._botmon = {t0: Date.now(), session: " . json_encode($this->sessionId) . ", seed: " . json_encode($this->getConf('captchaSeed')) . ", ip: " . json_encode($_SERVER['REMOTE_ADDR']) . "};" . NL;
103d49ab213SSascha Leib
104d49ab213SSascha Leib		// is there a user logged in?
105d49ab213SSascha Leib		$username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name']) ?  $INFO['userinfo']['name'] : '');
106d49ab213SSascha Leib		if ($username) {
107d49ab213SSascha Leib			$code .= DOKU_TAB . DOKU_TAB . 'document._botmon.user = "' . $username . '";'. NL;
108d49ab213SSascha Leib		}
109d49ab213SSascha Leib
110d49ab213SSascha Leib		return $code;
111d49ab213SSascha Leib
112d49ab213SSascha Leib	}
113d49ab213SSascha Leib
114451abfadSSascha Leib	/**
115e56d7b71SSascha Leib	 * Inserts tracking code to the page header
116e56d7b71SSascha Leib	 * (only called on 'show' actions)
117e56d7b71SSascha Leib	 *
118e56d7b71SSascha Leib	 * @param Event $event event object by reference
119e56d7b71SSascha Leib	 * @return void
120e56d7b71SSascha Leib	 */
121e56d7b71SSascha Leib	public function insertAdminHeader(Event $event, $param) {
122e56d7b71SSascha Leib
123e56d7b71SSascha Leib		$event->data['link'][] = ['rel' => 'stylesheet', 'href' => DOKU_BASE.'lib/plugins/botmon/admin.css', 'defer' => 'defer'];
1240edf1a56SSascha Leib		$event->data['script'][] = ['src' => DOKU_BASE.'lib/plugins/botmon/admin.js', 'defer' => 'defer', '_data' => ''];
125e56d7b71SSascha Leib	}
126e56d7b71SSascha Leib
127e56d7b71SSascha Leib	/**
128451abfadSSascha Leib	 * Writes data to the server log.
129451abfadSSascha Leib	 *
130451abfadSSascha Leib	 * @return void
131451abfadSSascha Leib	 */
1325f2c1759SSascha Leib	public function writeServerLog(Event $event, $param) {
133451abfadSSascha Leib
134451abfadSSascha Leib		global $conf;
135451abfadSSascha Leib		global $INFO;
136091b5998SSascha Leib
1375f2c1759SSascha Leib		// is there a user logged in?
1385f2c1759SSascha Leib		$username = ( !empty($INFO['userinfo']) && !empty($INFO['userinfo']['name'])
1395f2c1759SSascha Leib					?  $INFO['userinfo']['name'] : '');
1405f2c1759SSascha Leib
141b2e3bd8bSSascha Leib		// clean the page ID
142b2e3bd8bSSascha Leib		$pageId = preg_replace('/[\x00-\x1F]/', "\u{FFFD}", $INFO['id'] ?? '');
143b2e3bd8bSSascha Leib
144451abfadSSascha Leib		// create the log array:
145cf9f7fe8SSascha Leib		$logArr = Array(
146f5f4ca13SSascha Leib			$_SERVER['REMOTE_ADDR'], /* remote IP */
147b2e3bd8bSSascha Leib			$pageId, /* page ID */
148b148c85eSSascha Leib			$this->sessionId, /* Session ID */
149b148c85eSSascha Leib			$this->sessionType, /* session ID type */
1505f2c1759SSascha Leib			$username, /* user name */
1512f2bc93aSSascha Leib			$_SERVER['HTTP_USER_AGENT'] ?? '', /* User agent */
152451abfadSSascha Leib			$_SERVER['HTTP_REFERER'] ?? '', /* HTTP Referrer */
153451abfadSSascha Leib			substr($conf['lang'],0,2), /* page language */
154a93de874SSascha Leib			implode(',', array_unique(array_map( function($it) { return substr(trim($it),0,2); }, explode(',',trim($_SERVER['HTTP_ACCEPT_LANGUAGE'], " \t;,*"))))), /* accepted client languages */
1552c641262SSascha Leib			$this->getCountryCode(), /* GeoIP country code */
156ad279a21SSascha Leib			$this->showCaptcha, /* show captcha? */
157ad279a21SSascha Leib			$_SERVER['REQUEST_METHOD'] ?? '' /* request method */
158d49ab213SSascha Leib		);
159cf9f7fe8SSascha Leib
160cf9f7fe8SSascha Leib		//* create the log line */
1614cddc661SSascha Leib		$filename = __DIR__ .'/logs/' . gmdate('Y-m-d') . '.srv.txt'; /* use GMT date for filename */
162cf9f7fe8SSascha Leib		$logline = gmdate('Y-m-d H:i:s'); /* use GMT time for log entries */
163cf9f7fe8SSascha Leib		foreach ($logArr as $tab) {
164cf9f7fe8SSascha Leib			$logline .= "\t" . $tab;
165cf9f7fe8SSascha Leib		};
166cf9f7fe8SSascha Leib
167cf9f7fe8SSascha Leib		/* write the log line to the file */
168cf9f7fe8SSascha Leib		$logfile = fopen($filename, 'a');
169cf9f7fe8SSascha Leib		if (!$logfile) die();
170cf9f7fe8SSascha Leib		if (fwrite($logfile, $logline . "\n") === false) {
171cf9f7fe8SSascha Leib			fclose($logfile);
172cf9f7fe8SSascha Leib			die();
1736980370bSSascha Leib		}
174cf9f7fe8SSascha Leib
175cf9f7fe8SSascha Leib		/* Done */
176cf9f7fe8SSascha Leib		fclose($logfile);
177cf9f7fe8SSascha Leib	}
178b148c85eSSascha Leib
1795f2c1759SSascha Leib	private function getCountryCode() {
1805f2c1759SSascha Leib
181f5f4ca13SSascha Leib		$country = ( $_SERVER['REMOTE_ADDR'] == '127.0.0.1' ? 'local' : 'ZZ' ); // default if no geoip is available!
1825f2c1759SSascha Leib
1835f2c1759SSascha Leib		$lib = $this->getConf('geoiplib'); /* which library to use? (can only be phpgeoip or disabled) */
1845f2c1759SSascha Leib
1855f2c1759SSascha Leib		try {
1865f2c1759SSascha Leib
1875f2c1759SSascha Leib			// use GeoIP module?
1885f2c1759SSascha Leib			if ($lib == 'phpgeoip' && extension_loaded('geoip') && geoip_db_avail(GEOIP_COUNTRY_EDITION)) { // Use PHP GeoIP module
1895f2c1759SSascha Leib				$result = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']);
1905f2c1759SSascha Leib				$country = ($result ? $result : $country);
1915f2c1759SSascha Leib			}
1925f2c1759SSascha Leib		} catch (Exception $e) {
1935f2c1759SSascha Leib			Logger::error('BotMon Plugin: GeoIP Error', $e->getMessage());
1945f2c1759SSascha Leib		}
1955f2c1759SSascha Leib
1965f2c1759SSascha Leib		return $country;
1975f2c1759SSascha Leib	}
1985f2c1759SSascha Leib
199447b8b4fSSascha Leib	private function setSessionInfo() {
200b148c85eSSascha Leib
201b148c85eSSascha Leib		// what is the session identifier?
202b148c85eSSascha Leib		if (isset($_SESSION)) {
203b148c85eSSascha Leib			$sesKeys = array_keys($_SESSION); /* DokuWiki Session ID preferred */
204b148c85eSSascha Leib			foreach ($sesKeys as $key) {
205b148c85eSSascha Leib				if (substr($key, 0, 2) == 'DW') {
206b148c85eSSascha Leib					$this->sessionId = $key;
207b148c85eSSascha Leib					$this->sessionType = 'dw';
208b148c85eSSascha Leib					return;
209b148c85eSSascha Leib				}
210b148c85eSSascha Leib			}
211b148c85eSSascha Leib		}
212f6a7ebc1SSascha Leib		if (!$this->sessionId) { /* no DokuWiki Session ID, try PHP session ID */
213b148c85eSSascha Leib			$this->sessionId = session_id();
214b148c85eSSascha Leib			$this->sessionType = 'php';
215b148c85eSSascha Leib		}
216f5f4ca13SSascha Leib		if (!$this->sessionId) { /* no PHP session ID, try IP address */
217f5f4ca13SSascha Leib			$this->sessionId = $_SERVER['REMOTE_ADDR'];
218b148c85eSSascha Leib			$this->sessionType = 'ip';
219b148c85eSSascha Leib		}
220447b8b4fSSascha Leib
221447b8b4fSSascha Leib		if (!$this->sessionId) { /* if all fails, use random data */
222447b8b4fSSascha Leib			$this->sessionId = rand(100000000, 999999999);
223447b8b4fSSascha Leib			$this->sessionType = 'rnd';
224447b8b4fSSascha Leib		}
225447b8b4fSSascha Leib
226b148c85eSSascha Leib	}
227f5f4ca13SSascha Leib
228393de67cSSascha Leib	public function insertCaptchaCode(Event $event) {
229f5f4ca13SSascha Leib
23019b69b64SSascha Leib		$useCaptcha = $this->getConf('useCaptcha'); // which background to show?
231f5f4ca13SSascha Leib
23219b69b64SSascha Leib		// only if we previously determined that we need a captcha:
23319b69b64SSascha Leib		if ($this->showCaptcha == 'Y') {
2342c641262SSascha Leib
23512993035SSascha Leib			echo '<h1 class="sectionedit1">'; tpl_pagetitle(); echo "</h1>\n"; // always show the original page title
236f5f4ca13SSascha Leib			$event->preventDefault(); // don't show normal content
23712993035SSascha Leib			switch ($useCaptcha) {
238393de67cSSascha Leib				case 'loremipsum':
239393de67cSSascha Leib					$this->insertLoremIpsum();  // show dada filler instead of text
24012993035SSascha Leib					break;
24112993035SSascha Leib				case 'dada':
24212993035SSascha Leib					$this->insertDadaFiller();  // show dada filler instead of text
24312993035SSascha Leib					break;
24412993035SSascha Leib			}
245d49ab213SSascha Leib
24619b69b64SSascha Leib			// insert the captcha loader code:
24719b69b64SSascha Leib			echo '<script>' . NL;
24819b69b64SSascha Leib
24919b69b64SSascha Leib			// add the deferred script loader::
25019b69b64SSascha Leib			echo  DOKU_TAB . "addEventListener('DOMContentLoaded', function(){" . NL;
25119b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "const cj=document.createElement('script');" . NL;
25219b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "cj.async=true;cj.defer=true;cj.type='text/javascript';" . NL;
25319b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "cj.src='".DOKU_BASE."lib/plugins/botmon/captcha.js';" . NL;
25419b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(cj);" . NL;
2556b6cd387SSascha Leib			echo  DOKU_TAB . "});" . NL;
25619b69b64SSascha Leib
25719b69b64SSascha Leib			// add the translated strings for the captcha:
25819b69b64SSascha Leib			echo  DOKU_TAB . '$BMLocales = {' . NL;
25919b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgTitle": ' . json_encode($this->getLang('bm_dlgTitle')) . ',' . NL;
26019b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgSubtitle": ' . json_encode($this->getLang('bm_dlgSubtitle')) . ',' . NL;
26119b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgConfirm": ' . json_encode($this->getLang('bm_dlgConfirm')) . ',' . NL;
26219b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgChecking": ' . json_encode($this->getLang('bm_dlgChecking')) . ',' . NL;
26319b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgLoading": ' . json_encode($this->getLang('bm_dlgLoading')) . ',' . NL;
26419b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgError": ' . json_encode($this->getLang('bm_dlgError')) . ',' . NL;
26519b69b64SSascha Leib			echo  DOKU_TAB . '};' . NL;
26619b69b64SSascha Leib
267fb281ca1SSascha Leib			// captcha configuration options
268fb281ca1SSascha Leib			echo  DOKU_TAB . '$BMConfig = {' . NL;
269fb281ca1SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"captchaBypass": ' . json_encode($this->getConf('captchaBypass')) . NL;
270fb281ca1SSascha Leib			echo  DOKU_TAB . '};' . NL;
271fb281ca1SSascha Leib
27219b69b64SSascha Leib			echo '</script>' . NL;
273*cb916979SSascha Leib
274*cb916979SSascha Leib			// insert a warning message for users without JavaScript:
275*cb916979SSascha Leib			echo '<dialog open closedby="any" id="BM__NoJSWarning"><p>' . $this->getLang('bm_noJsWarning') . '</p></dialog>' . NL;
276*cb916979SSascha Leib
27719b69b64SSascha Leib		}
278d49ab213SSascha Leib	}
279f5f4ca13SSascha Leib
280d49ab213SSascha Leib	public function showImageCaptcha(Event $event, $param) {
281d49ab213SSascha Leib
282d49ab213SSascha Leib		$useCaptcha = $this->getConf('useCaptcha');
283d49ab213SSascha Leib
284d49ab213SSascha Leib		echo '<script>' . $this->getBMHeader($event, $param) . '</script>';
285d49ab213SSascha Leib
286d49ab213SSascha Leib		$cCode = '-';
287d49ab213SSascha Leib		if ($useCaptcha !== 'disabled') {
288d49ab213SSascha Leib			if ($this->captchaWhitelisted()) {
289d49ab213SSascha Leib				$cCode = 'W'; // whitelisted
290d49ab213SSascha Leib			}
291d49ab213SSascha Leib			elseif ($this->hasCaptchaCookie()) {
292d49ab213SSascha Leib				$cCode  = 'N'; // user already has a cookie
293d49ab213SSascha Leib			}
294d49ab213SSascha Leib			else {
295d49ab213SSascha Leib				$cCode  = 'Y'; // show the captcha
296d49ab213SSascha Leib
297d49ab213SSascha Leib				echo '<svg width="100%" height="100%" viewBox="0 0 800 400" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M1,1l798,398" style="fill:none;stroke:#f00;stroke-width:1px;"/><path d="M1,399l798,-398" style="fill:none;stroke:#f00;stroke-width:1px;"/><rect x="1" y="1" width="798" height="398" style="fill:none;stroke:#000;stroke-width:1px;"/></svg>'; // placeholder image
298d49ab213SSascha Leib				$event->preventDefault(); // don't show normal content
299d49ab213SSascha Leib
300d49ab213SSascha Leib				// TODO Insert dummy image
301d49ab213SSascha Leib				$this->insertCaptchaLoader(); // and load the captcha
302d49ab213SSascha Leib			}
303d49ab213SSascha Leib		};
304d49ab213SSascha Leib
305d49ab213SSascha Leib		$this->showCaptcha = $cCode; // store the captcha code for the logfile
306d49ab213SSascha Leib	}
307d49ab213SSascha Leib
3086728cfa6SSascha Leib	/**
3096728cfa6SSascha Leib	 * Checks if the user has a valid captcha cookie.
3106728cfa6SSascha Leib	 *
3116728cfa6SSascha Leib	 * @return boolean
3126728cfa6SSascha Leib	 * @access private
3136728cfa6SSascha Leib	 *
3146728cfa6SSascha Leib	 **/
315d49ab213SSascha Leib	private function hasCaptchaCookie() {
316f5f4ca13SSascha Leib
317cdc02cd4SSascha Leib		$cookieVal = isset($_COOKIE['DWConfirm']) ? $_COOKIE['DWConfirm'] : null;
318f5f4ca13SSascha Leib
319cdc02cd4SSascha Leib		$today = substr((new DateTime())->format('c'), 0, 10);
32012993035SSascha Leib
321871c97bfSSascha Leib		$raw = $this->getConf('captchaSeed') . ';' . $_SERVER['SERVER_NAME'] . ';' . $_SERVER['REMOTE_ADDR'] . ';' . $today;
322871c97bfSSascha Leib		$expected = hash('sha256', $raw);
32312993035SSascha Leib
3246728cfa6SSascha Leib		// for debugging: write captcha data to the log:
3256728cfa6SSascha Leib		$this->writeCaptchaLog($_SERVER['REMOTE_ADDR'], $cookieVal, $_SERVER['SERVER_NAME'], $expected);
326cdc02cd4SSascha Leib
327d49ab213SSascha Leib		return $cookieVal == $expected;
328f5f4ca13SSascha Leib	}
329f5f4ca13SSascha Leib
3306728cfa6SSascha Leib	/**
3316728cfa6SSascha Leib	 * Writes data to the captcha log.
3326728cfa6SSascha Leib	 *
3336728cfa6SSascha Leib	 * @return void
3346728cfa6SSascha Leib	 */
3356728cfa6SSascha Leib	private function writeCaptchaLog($remote_addr, $cookieVal, $serverName, $expected) {
3366728cfa6SSascha Leib
3374ab56ef9SSascha Leib		global $INFO;
3384ab56ef9SSascha Leib
3396728cfa6SSascha Leib		$logArr = Array(
3406728cfa6SSascha Leib			$remote_addr, /* remote IP */
3416728cfa6SSascha Leib			$cookieVal, /* cookie value */
3426728cfa6SSascha Leib			$this->getConf('captchaSeed'), /* seed */
3436728cfa6SSascha Leib			$serverName, /* server name */
3446728cfa6SSascha Leib			$expected, /* expected cookie value */
3454ab56ef9SSascha Leib			($cookieVal == $expected ? 'MATCH' : 'WRONG'), /* cookie matches expected value? */
3464ab56ef9SSascha Leib			$_SERVER['REQUEST_URI'] /* request URI */
3476728cfa6SSascha Leib		);
3486728cfa6SSascha Leib
3496728cfa6SSascha Leib		//* create the log line */
3506728cfa6SSascha Leib		$filename = __DIR__ .'/logs/' . gmdate('Y-m-d') . '.captcha.txt'; /* use GMT date for filename */
3516728cfa6SSascha Leib		$logline = gmdate('Y-m-d H:i:s'); /* use GMT time for log entries */
3526728cfa6SSascha Leib		foreach ($logArr as $tab) {
3536728cfa6SSascha Leib			$logline .= "\t" . $tab;
3546728cfa6SSascha Leib		};
3556728cfa6SSascha Leib
3566728cfa6SSascha Leib		/* write the log line to the file */
3576728cfa6SSascha Leib		$logfile = fopen($filename, 'a');
3586728cfa6SSascha Leib		if (!$logfile) die();
3596728cfa6SSascha Leib		if (fwrite($logfile, $logline . "\n") === false) {
3606728cfa6SSascha Leib			fclose($logfile);
3616728cfa6SSascha Leib			die();
3626728cfa6SSascha Leib		}
3636728cfa6SSascha Leib
3649b2115f1SSascha Leib		// in case of errors, write the cookie data to the log:
3659b2115f1SSascha Leib		if (!$cookieVal) {
3660cfc0c5dSSascha Leib			$logline =  print_r($_COOKIE, true);
3679b2115f1SSascha Leib			if (fwrite($logfile, $logline . "\n") === false) {
3689b2115f1SSascha Leib				fclose($logfile);
3699b2115f1SSascha Leib				die();
3709b2115f1SSascha Leib			}
3719b2115f1SSascha Leib		}
3729b2115f1SSascha Leib
3739b2115f1SSascha Leib		/* Done. close the file. */
3746728cfa6SSascha Leib		fclose($logfile);
3756728cfa6SSascha Leib	}
3766728cfa6SSascha Leib
3776728cfa6SSascha Leib
3782c641262SSascha Leib	// check if the visitor's IP is on a whitelist:
3792c641262SSascha Leib	private function captchaWhitelisted() {
3802c641262SSascha Leib
3812c641262SSascha Leib		// normalise IP address:
3822c641262SSascha Leib		$ip = inet_pton($_SERVER['REMOTE_ADDR']);
3832c641262SSascha Leib
3842c641262SSascha Leib		// find which file to open:
3852c641262SSascha Leib		$prefixes = ['user', 'default'];
3862c641262SSascha Leib		foreach ($prefixes as $pre) {
3872c641262SSascha Leib			$filename = __DIR__ .'/config/' . $pre . '-whitelist.txt';
3882c641262SSascha Leib			if (file_exists($filename)) {
3892c641262SSascha Leib				break;
3902c641262SSascha Leib			}
3912c641262SSascha Leib		}
3922c641262SSascha Leib
3932c641262SSascha Leib		if (file_exists($filename)) {
3942c641262SSascha Leib			$lines = file($filename, FILE_SKIP_EMPTY_LINES);
3952c641262SSascha Leib			foreach ($lines as $line) {
3962c641262SSascha Leib				if (trim($line) !== '' && !str_starts_with($line, '#')) {
3972c641262SSascha Leib					$col = explode("\t", $line);
3982c641262SSascha Leib					if (count($col) >= 2) {
3992c641262SSascha Leib						$from = inet_pton($col[0]);
4002c641262SSascha Leib						$to = inet_pton($col[1]);
4012c641262SSascha Leib
4022c641262SSascha Leib						if ($ip >= $from && $ip <= $to) {
403d49ab213SSascha Leib							return true; /* IP whitelisted */
4042c641262SSascha Leib						}
4052c641262SSascha Leib					}
4062c641262SSascha Leib				}
4072c641262SSascha Leib			}
4082c641262SSascha Leib		}
409d49ab213SSascha Leib		return false; /* IP not found in whitelist */
4102c641262SSascha Leib	}
4112c641262SSascha Leib
41212993035SSascha Leib	// inserts a blank box to ensure there is enough space for the captcha:
413393de67cSSascha Leib	private function insertLoremIpsum() {
41412993035SSascha Leib
415393de67cSSascha Leib		echo '<div class="level1">' . NL;
416393de67cSSascha Leib		echo '<p>' . NL . 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'. NL . '</p>' . NL;
417393de67cSSascha Leib		echo '<p>' . NL . 'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'. NL . '</p>' . NL;
418393de67cSSascha Leib		echo '</div>' . NL;
419393de67cSSascha Leib
42012993035SSascha Leib	}
42112993035SSascha Leib
42212993035SSascha Leib	/* Generates a few paragraphs of Dada text to show instead of the article content */
423f5f4ca13SSascha Leib	private function insertDadaFiller() {
424f5f4ca13SSascha Leib
42512993035SSascha Leib		global $conf;
42612993035SSascha Leib		global $TOC;
42712993035SSascha Leib		global $ID;
428f5f4ca13SSascha Leib
42912993035SSascha Leib		// list of languages to search for the wordlist
43012993035SSascha Leib		$langs = array_unique([$conf['lang'], 'la']);
431f5f4ca13SSascha Leib
43212993035SSascha Leib		// find path to the first available wordlist:
43312993035SSascha Leib		foreach ($langs as $lang) {
43412993035SSascha Leib			$filename = __DIR__ .'/lang/' . $lang . '/wordlist.txt'; /* language-specific wordlist */
43512993035SSascha Leib			if (file_exists($filename)) {
43612993035SSascha Leib				break;
43712993035SSascha Leib			}
43812993035SSascha Leib		}
439f5f4ca13SSascha Leib
44012993035SSascha Leib		// load the wordlist file:
44112993035SSascha Leib		if (file_exists($filename)) {
44212993035SSascha Leib			$words = array();
44312993035SSascha Leib			$totalWeight = 0;
44412993035SSascha Leib			$lines = file($filename, FILE_SKIP_EMPTY_LINES);
44512993035SSascha Leib			foreach ($lines as $line) {
44612993035SSascha Leib				$arr = explode("\t", $line);
44712993035SSascha Leib				$arr[1] = ( count($arr) > 1 ? (int) trim($arr[1]) : 1 );
44812993035SSascha Leib				$totalWeight += (int) $arr[1];
44912993035SSascha Leib				array_push($words, $arr);
45012993035SSascha Leib			}
45112993035SSascha Leib		} else {
45212993035SSascha Leib			echo '<script> console.log("Can’t generate filler text: wordlist file not found!"); </script>';
45312993035SSascha Leib			return;
45412993035SSascha Leib		}
455f5f4ca13SSascha Leib
45612993035SSascha Leib		// If a TOC exists, use it for the headlines:
45712993035SSascha Leib		if(is_array($TOC)) {
45812993035SSascha Leib			$toc = $TOC;
45912993035SSascha Leib		} else {
46012993035SSascha Leib			$meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
46112993035SSascha Leib			//$tocok = (isset($meta['internal']['toc']) ? $meta['internal']['toc'] : $tocok = true);
46212993035SSascha Leib			$toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null;
46312993035SSascha Leib		}
46412993035SSascha Leib		if (!$toc) { // no TOC, generate my own:
46512993035SSascha Leib			$hlCount = mt_rand(0, (int) $conf['tocminheads']);
46612993035SSascha Leib			$toc = array();
46712993035SSascha Leib			for ($i=0; $i<$hlCount; $i++) {
46812993035SSascha Leib				array_push($toc, $this->dadaMakeHeadline($words, $totalWeight)); // $toc
46912993035SSascha Leib			}
47012993035SSascha Leib		}
47112993035SSascha Leib
47212993035SSascha Leib		// if H1 heading is not in the TOC, add a chappeau section:
47312993035SSascha Leib		$chapeauCount = mt_rand(1, 3);
47412993035SSascha Leib		if ((int) $conf['toptoclevel'] > 1) {
47512993035SSascha Leib			echo "<div class=\"level1\">\n";
47612993035SSascha Leib			for ($i=0; $i<$chapeauCount; $i++) {
47712993035SSascha Leib				echo $this->dadaMakeParagraph($words, $totalWeight);
47812993035SSascha Leib			}
47912993035SSascha Leib			echo "</div>\n";
48012993035SSascha Leib		}
48112993035SSascha Leib
48212993035SSascha Leib		//  text sections for each sub-headline:
48312993035SSascha Leib		foreach ($toc as $hl) {
48412993035SSascha Leib			echo $this->dadaMakeSection($words, $totalWeight, $hl);
48512993035SSascha Leib		}
48612993035SSascha Leib	}
48712993035SSascha Leib
48812993035SSascha Leib	private function dadaMakeSection($words, $totalWeight, $hl) {
48912993035SSascha Leib
49012993035SSascha Leib		global $conf;
49112993035SSascha Leib
49212993035SSascha Leib		// how many paragraphs?
49312993035SSascha Leib		$paragraphCount = mt_rand(1, 4);
49412993035SSascha Leib
49512993035SSascha Leib		// section level
49612993035SSascha Leib		$topTocLevel = (int) $conf['toptoclevel'];
49712993035SSascha Leib		$secLevel = $hl['level'] + 1;;
49812993035SSascha Leib
49912993035SSascha Leib		// return value:
50012993035SSascha Leib		$sec = "";
50112993035SSascha Leib
50212993035SSascha Leib		// make a headline:
50312993035SSascha Leib		if ($topTocLevel > 1 || $secLevel > 1) {
50412993035SSascha Leib			$sec .= "<h{$secLevel} id=\"{$hl['hid']}\">{$hl['title']}</h{$secLevel}>\n";
50512993035SSascha Leib		}
50612993035SSascha Leib
50712993035SSascha Leib		// add the paragraphs:
50812993035SSascha Leib		$sec .= "<div class=\"level{$secLevel}\">\n";
50912993035SSascha Leib		for ($i=0; $i<$paragraphCount; $i++) {
51012993035SSascha Leib			$sec .= $this->dadaMakeParagraph($words, $totalWeight);
51112993035SSascha Leib		}
51212993035SSascha Leib		$sec .= "</div>\n";
51312993035SSascha Leib
51412993035SSascha Leib		return $sec;
51512993035SSascha Leib	}
51612993035SSascha Leib
51712993035SSascha Leib	private function dadaMakeHeadline($words, $totalWeight) {
51812993035SSascha Leib
51912993035SSascha Leib		// how many words to generate?
52012993035SSascha Leib		$wordCount = mt_rand(2, 5);
52112993035SSascha Leib
52212993035SSascha Leib		// function returns an array:
52312993035SSascha Leib		$r = Array();
52412993035SSascha Leib
52512993035SSascha Leib		// generate the headline:
52612993035SSascha Leib		$hlArr = array();
52712993035SSascha Leib		for ($i=0; $i<$wordCount; $i++) {
52812993035SSascha Leib			array_push($hlArr, $this->dadaSelectRandomWord($words, $totalWeight));
52912993035SSascha Leib		}
53012993035SSascha Leib
53112993035SSascha Leib		$r['title'] =  ucfirst(implode(' ', $hlArr));
53212993035SSascha Leib
53312993035SSascha Leib		$r['hid'] = preg_replace('/[^\w\d\-]+/i', '_', strtolower($r['title']));
53412993035SSascha Leib		$r['type'] = 'ul'; // always ul!
53512993035SSascha Leib		$r['level'] = 1; // always level 1 for now
53612993035SSascha Leib
53712993035SSascha Leib		return $r;
53812993035SSascha Leib	}
53912993035SSascha Leib
54012993035SSascha Leib	private function dadaMakeParagraph($words, $totalWeight) {
54112993035SSascha Leib
54212993035SSascha Leib		// how many words to generate?
54312993035SSascha Leib		$sentenceCount = mt_rand(2, 5);
54412993035SSascha Leib
54512993035SSascha Leib		$paragraph = array();
54612993035SSascha Leib		for ($i=0; $i<$sentenceCount; $i++) {
54712993035SSascha Leib			array_push($paragraph, $this->dadaMakeSentence($words, $totalWeight));
54812993035SSascha Leib		}
54912993035SSascha Leib
55012993035SSascha Leib		return "<p>\n" . implode(' ', $paragraph) . "\n</p>\n";
55112993035SSascha Leib
55212993035SSascha Leib	}
55312993035SSascha Leib
55412993035SSascha Leib	private function dadaMakeSentence($words, $totalWeight) {
55512993035SSascha Leib
55612993035SSascha Leib		// how many words to generate?
55712993035SSascha Leib		$wordCount = mt_rand(4, 20);
55812993035SSascha Leib
55912993035SSascha Leib		// generate the sentence:
56012993035SSascha Leib		$sentence = array();
56112993035SSascha Leib		for ($i=0; $i<$wordCount; $i++) {
56212993035SSascha Leib			array_push($sentence, $this->dadaSelectRandomWord($words, $totalWeight));
56312993035SSascha Leib		}
56412993035SSascha Leib
56512993035SSascha Leib		return ucfirst(implode(' ', $sentence)) . '.';
56612993035SSascha Leib
56712993035SSascha Leib	}
56812993035SSascha Leib
56912993035SSascha Leib	private function dadaSelectRandomWord($list, $totalWeight) {
57012993035SSascha Leib
57112993035SSascha Leib		// get a random selection:
57212993035SSascha Leib		$rand = mt_rand(0, $totalWeight);
57312993035SSascha Leib
57412993035SSascha Leib		// match the selection to the weighted list:
57512993035SSascha Leib		$cumulativeWeight = 0;
57612993035SSascha Leib		for ($i=0; $i<count($list); $i++) {
57712993035SSascha Leib			$cumulativeWeight += $list[$i][1];
57812993035SSascha Leib			if ($cumulativeWeight >= $rand) {
57912993035SSascha Leib				return $list[$i][0];
58012993035SSascha Leib			}
58112993035SSascha Leib		}
58212993035SSascha Leib		return '***';
583f5f4ca13SSascha Leib	}
584f5f4ca13SSascha Leib
5856980370bSSascha Leib}