xref: /plugin/botmon/action.php (revision e0f06f2dc70bac79533ba109b82f127bc2294033)
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
144*e0f06f2dSSascha Leib		// get accepted languages:
145*e0f06f2dSSascha Leib		$acceptedLanguages = ( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '' );
146*e0f06f2dSSascha Leib
147451abfadSSascha Leib		// create the log array:
148cf9f7fe8SSascha Leib		$logArr = Array(
149f5f4ca13SSascha Leib			$_SERVER['REMOTE_ADDR'], /* remote IP */
150b2e3bd8bSSascha Leib			$pageId, /* page ID */
151b148c85eSSascha Leib			$this->sessionId, /* Session ID */
152b148c85eSSascha Leib			$this->sessionType, /* session ID type */
1535f2c1759SSascha Leib			$username, /* user name */
1542f2bc93aSSascha Leib			$_SERVER['HTTP_USER_AGENT'] ?? '', /* User agent */
155451abfadSSascha Leib			$_SERVER['HTTP_REFERER'] ?? '', /* HTTP Referrer */
156451abfadSSascha Leib			substr($conf['lang'],0,2), /* page language */
157*e0f06f2dSSascha Leib			implode(',', array_unique(array_map( function($it) { return substr(trim($it),0,2); }, explode(',',trim($acceptedLanguages, " \t;,*"))))), /* accepted client languages */
1582c641262SSascha Leib			$this->getCountryCode(), /* GeoIP country code */
159ad279a21SSascha Leib			$this->showCaptcha, /* show captcha? */
160ad279a21SSascha Leib			$_SERVER['REQUEST_METHOD'] ?? '' /* request method */
161d49ab213SSascha Leib		);
162cf9f7fe8SSascha Leib
163cf9f7fe8SSascha Leib		//* create the log line */
1644cddc661SSascha Leib		$filename = __DIR__ .'/logs/' . gmdate('Y-m-d') . '.srv.txt'; /* use GMT date for filename */
165cf9f7fe8SSascha Leib		$logline = gmdate('Y-m-d H:i:s'); /* use GMT time for log entries */
166cf9f7fe8SSascha Leib		foreach ($logArr as $tab) {
167cf9f7fe8SSascha Leib			$logline .= "\t" . $tab;
168cf9f7fe8SSascha Leib		};
169cf9f7fe8SSascha Leib
170cf9f7fe8SSascha Leib		/* write the log line to the file */
171cf9f7fe8SSascha Leib		$logfile = fopen($filename, 'a');
172cf9f7fe8SSascha Leib		if (!$logfile) die();
173cf9f7fe8SSascha Leib		if (fwrite($logfile, $logline . "\n") === false) {
174cf9f7fe8SSascha Leib			fclose($logfile);
175cf9f7fe8SSascha Leib			die();
1766980370bSSascha Leib		}
177cf9f7fe8SSascha Leib
178cf9f7fe8SSascha Leib		/* Done */
179cf9f7fe8SSascha Leib		fclose($logfile);
180cf9f7fe8SSascha Leib	}
181b148c85eSSascha Leib
1825f2c1759SSascha Leib	private function getCountryCode() {
1835f2c1759SSascha Leib
184f5f4ca13SSascha Leib		$country = ( $_SERVER['REMOTE_ADDR'] == '127.0.0.1' ? 'local' : 'ZZ' ); // default if no geoip is available!
1855f2c1759SSascha Leib
18669b73efaSSascha Leib		$lib = $this->getConf('geoiplib'); /* which library to use? (possible values are: disabled, phpgeoip or cloudflare) */
1875f2c1759SSascha Leib
1885f2c1759SSascha Leib		try {
1895f2c1759SSascha Leib
19069b73efaSSascha Leib			switch($lib) {
19169b73efaSSascha Leib
19269b73efaSSascha Leib			case 'phpgeoip':
19369b73efaSSascha Leib				if (extension_loaded('geoip') && geoip_db_avail(GEOIP_COUNTRY_EDITION)) { // PHP GeoIP module available?
1945f2c1759SSascha Leib					$result = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']);
1955f2c1759SSascha Leib					$country = ($result ? $result : $country);
1965f2c1759SSascha Leib				}
19769b73efaSSascha Leib				break;
19869b73efaSSascha Leib
19969b73efaSSascha Leib			case 'cloudflare':
20069b73efaSSascha Leib				$result = $_SERVER['HTTP_CF_IPCOUNTRY'] ?? null;
20169b73efaSSascha Leib				$country = ( $result == 'XX' || $result === null ? 'ZZ' : $result ); // Cloudflare returns 'XX' for unknown countries, we want 'ZZ' in that case
20269b73efaSSascha Leib				break;
20369b73efaSSascha Leib
20469b73efaSSascha Leib			}
20569b73efaSSascha Leib
20669b73efaSSascha Leib
2075f2c1759SSascha Leib		} catch (Exception $e) {
2085f2c1759SSascha Leib			Logger::error('BotMon Plugin: GeoIP Error', $e->getMessage());
2095f2c1759SSascha Leib		}
2105f2c1759SSascha Leib
2115f2c1759SSascha Leib		return $country;
2125f2c1759SSascha Leib	}
2135f2c1759SSascha Leib
214447b8b4fSSascha Leib	private function setSessionInfo() {
215b148c85eSSascha Leib
216b148c85eSSascha Leib		// what is the session identifier?
217b148c85eSSascha Leib		if (isset($_SESSION)) {
218b148c85eSSascha Leib			$sesKeys = array_keys($_SESSION); /* DokuWiki Session ID preferred */
219b148c85eSSascha Leib			foreach ($sesKeys as $key) {
220b148c85eSSascha Leib				if (substr($key, 0, 2) == 'DW') {
221b148c85eSSascha Leib					$this->sessionId = $key;
222b148c85eSSascha Leib					$this->sessionType = 'dw';
223b148c85eSSascha Leib					return;
224b148c85eSSascha Leib				}
225b148c85eSSascha Leib			}
226b148c85eSSascha Leib		}
227f6a7ebc1SSascha Leib		if (!$this->sessionId) { /* no DokuWiki Session ID, try PHP session ID */
228b148c85eSSascha Leib			$this->sessionId = session_id();
229b148c85eSSascha Leib			$this->sessionType = 'php';
230b148c85eSSascha Leib		}
231f5f4ca13SSascha Leib		if (!$this->sessionId) { /* no PHP session ID, try IP address */
232f5f4ca13SSascha Leib			$this->sessionId = $_SERVER['REMOTE_ADDR'];
233b148c85eSSascha Leib			$this->sessionType = 'ip';
234b148c85eSSascha Leib		}
235447b8b4fSSascha Leib
236447b8b4fSSascha Leib		if (!$this->sessionId) { /* if all fails, use random data */
237447b8b4fSSascha Leib			$this->sessionId = rand(100000000, 999999999);
238447b8b4fSSascha Leib			$this->sessionType = 'rnd';
239447b8b4fSSascha Leib		}
240447b8b4fSSascha Leib
241b148c85eSSascha Leib	}
242f5f4ca13SSascha Leib
243393de67cSSascha Leib	public function insertCaptchaCode(Event $event) {
244f5f4ca13SSascha Leib
24519b69b64SSascha Leib		$useCaptcha = $this->getConf('useCaptcha'); // which background to show?
246f5f4ca13SSascha Leib
24719b69b64SSascha Leib		// only if we previously determined that we need a captcha:
24819b69b64SSascha Leib		if ($this->showCaptcha == 'Y') {
2492c641262SSascha Leib
25012993035SSascha Leib			echo '<h1 class="sectionedit1">'; tpl_pagetitle(); echo "</h1>\n"; // always show the original page title
251f5f4ca13SSascha Leib			$event->preventDefault(); // don't show normal content
25212993035SSascha Leib			switch ($useCaptcha) {
253393de67cSSascha Leib				case 'loremipsum':
254393de67cSSascha Leib					$this->insertLoremIpsum();  // show dada filler instead of text
25512993035SSascha Leib					break;
25612993035SSascha Leib				case 'dada':
25712993035SSascha Leib					$this->insertDadaFiller();  // show dada filler instead of text
25812993035SSascha Leib					break;
25912993035SSascha Leib			}
260d49ab213SSascha Leib
26119b69b64SSascha Leib			// insert the captcha loader code:
26219b69b64SSascha Leib			echo '<script>' . NL;
26319b69b64SSascha Leib
26419b69b64SSascha Leib			// add the deferred script loader::
26519b69b64SSascha Leib			echo  DOKU_TAB . "addEventListener('DOMContentLoaded', function(){" . NL;
26619b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "const cj=document.createElement('script');" . NL;
26719b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "cj.async=true;cj.defer=true;cj.type='text/javascript';" . NL;
26819b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "cj.src='".DOKU_BASE."lib/plugins/botmon/captcha.js';" . NL;
26919b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . "document.getElementsByTagName('head')[0].appendChild(cj);" . NL;
2706b6cd387SSascha Leib			echo  DOKU_TAB . "});" . NL;
27119b69b64SSascha Leib
27219b69b64SSascha Leib			// add the translated strings for the captcha:
27319b69b64SSascha Leib			echo  DOKU_TAB . '$BMLocales = {' . NL;
27419b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgTitle": ' . json_encode($this->getLang('bm_dlgTitle')) . ',' . NL;
27519b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgSubtitle": ' . json_encode($this->getLang('bm_dlgSubtitle')) . ',' . NL;
27619b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgConfirm": ' . json_encode($this->getLang('bm_dlgConfirm')) . ',' . NL;
27719b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgChecking": ' . json_encode($this->getLang('bm_dlgChecking')) . ',' . NL;
27819b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgLoading": ' . json_encode($this->getLang('bm_dlgLoading')) . ',' . NL;
27919b69b64SSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"dlgError": ' . json_encode($this->getLang('bm_dlgError')) . ',' . NL;
28019b69b64SSascha Leib			echo  DOKU_TAB . '};' . NL;
28119b69b64SSascha Leib
282fb281ca1SSascha Leib			// captcha configuration options
283fb281ca1SSascha Leib			echo  DOKU_TAB . '$BMConfig = {' . NL;
2847dae1c4dSSascha Leib			echo  DOKU_TAB . DOKU_TAB . '"captchaOptions": ' . json_encode($this->getConf('captchaOptions')) . NL;
285fb281ca1SSascha Leib			echo  DOKU_TAB . '};' . NL;
286fb281ca1SSascha Leib
28719b69b64SSascha Leib			echo '</script>' . NL;
288cb916979SSascha Leib
289cb916979SSascha Leib			// insert a warning message for users without JavaScript:
290cb916979SSascha Leib			echo '<dialog open closedby="any" id="BM__NoJSWarning"><p>' . $this->getLang('bm_noJsWarning') . '</p></dialog>' . NL;
291cb916979SSascha Leib
29219b69b64SSascha Leib		}
293d49ab213SSascha Leib	}
294f5f4ca13SSascha Leib
295d49ab213SSascha Leib	public function showImageCaptcha(Event $event, $param) {
296d49ab213SSascha Leib
297d49ab213SSascha Leib		$useCaptcha = $this->getConf('useCaptcha');
298d49ab213SSascha Leib
299d49ab213SSascha Leib		echo '<script>' . $this->getBMHeader($event, $param) . '</script>';
300d49ab213SSascha Leib
301d49ab213SSascha Leib		$cCode = '-';
302d49ab213SSascha Leib		if ($useCaptcha !== 'disabled') {
303d49ab213SSascha Leib			if ($this->captchaWhitelisted()) {
304d49ab213SSascha Leib				$cCode = 'W'; // whitelisted
305d49ab213SSascha Leib			}
306d49ab213SSascha Leib			elseif ($this->hasCaptchaCookie()) {
307d49ab213SSascha Leib				$cCode  = 'N'; // user already has a cookie
308d49ab213SSascha Leib			}
309d49ab213SSascha Leib			else {
310d49ab213SSascha Leib				$cCode  = 'Y'; // show the captcha
311d49ab213SSascha Leib
312d49ab213SSascha 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
313d49ab213SSascha Leib				$event->preventDefault(); // don't show normal content
314d49ab213SSascha Leib
315d49ab213SSascha Leib				// TODO Insert dummy image
316d49ab213SSascha Leib				$this->insertCaptchaLoader(); // and load the captcha
317d49ab213SSascha Leib			}
318d49ab213SSascha Leib		};
319d49ab213SSascha Leib
320d49ab213SSascha Leib		$this->showCaptcha = $cCode; // store the captcha code for the logfile
321d49ab213SSascha Leib	}
322d49ab213SSascha Leib
3236728cfa6SSascha Leib	/**
3246728cfa6SSascha Leib	 * Checks if the user has a valid captcha cookie.
3256728cfa6SSascha Leib	 *
3266728cfa6SSascha Leib	 * @return boolean
3276728cfa6SSascha Leib	 * @access private
3286728cfa6SSascha Leib	 *
3296728cfa6SSascha Leib	 **/
330d49ab213SSascha Leib	private function hasCaptchaCookie() {
331f5f4ca13SSascha Leib
332cdc02cd4SSascha Leib		$cookieVal = isset($_COOKIE['DWConfirm']) ? $_COOKIE['DWConfirm'] : null;
333f5f4ca13SSascha Leib
3347dae1c4dSSascha Leib		// bypass cookie checking, of config option is set:
3357dae1c4dSSascha Leib		$captchaOptions = explode(',', $this->getConf('captchaOptions'));
336ac328188SSascha Leib		if (in_array('anyval', $captchaOptions) && strlen($cookieVal) == 64) {
337ac328188SSascha Leib			//$this->writeCaptchaLog($_SERVER['REMOTE_ADDR'], $cookieVal, $_SERVER['SERVER_NAME'], "BYPASSED:" . strlen($cookieVal)); // Debug only
3387dae1c4dSSascha Leib			return true;
3397dae1c4dSSascha Leib		}
34012993035SSascha Leib
3417dae1c4dSSascha Leib		//  calculate the expected cookie value:
3427dae1c4dSSascha Leib		$today = substr((new DateTime())->format('c'), 0, 10);
343871c97bfSSascha Leib		$raw = $this->getConf('captchaSeed') . ';' . $_SERVER['SERVER_NAME'] . ';' . $_SERVER['REMOTE_ADDR'] . ';' . $today;
344871c97bfSSascha Leib		$expected = hash('sha256', $raw);
34512993035SSascha Leib
3466728cfa6SSascha Leib		// for debugging: write captcha data to the log:
3477dae1c4dSSascha Leib		//$this->writeCaptchaLog($_SERVER['REMOTE_ADDR'], $cookieVal, $_SERVER['SERVER_NAME'], $expected);
348cdc02cd4SSascha Leib
349d49ab213SSascha Leib		return $cookieVal == $expected;
350f5f4ca13SSascha Leib	}
351f5f4ca13SSascha Leib
3526728cfa6SSascha Leib	/**
3536728cfa6SSascha Leib	 * Writes data to the captcha log.
3546728cfa6SSascha Leib	 *
3556728cfa6SSascha Leib	 * @return void
3566728cfa6SSascha Leib	 */
3576728cfa6SSascha Leib	private function writeCaptchaLog($remote_addr, $cookieVal, $serverName, $expected) {
3586728cfa6SSascha Leib
3594ab56ef9SSascha Leib		global $INFO;
3604ab56ef9SSascha Leib
3616728cfa6SSascha Leib		$logArr = Array(
3626728cfa6SSascha Leib			$remote_addr, /* remote IP */
3636728cfa6SSascha Leib			$cookieVal, /* cookie value */
3646728cfa6SSascha Leib			$this->getConf('captchaSeed'), /* seed */
3656728cfa6SSascha Leib			$serverName, /* server name */
3666728cfa6SSascha Leib			$expected, /* expected cookie value */
3674ab56ef9SSascha Leib			($cookieVal == $expected ? 'MATCH' : 'WRONG'), /* cookie matches expected value? */
3684ab56ef9SSascha Leib			$_SERVER['REQUEST_URI'] /* request URI */
3696728cfa6SSascha Leib		);
3706728cfa6SSascha Leib
3716728cfa6SSascha Leib		//* create the log line */
3726728cfa6SSascha Leib		$filename = __DIR__ .'/logs/' . gmdate('Y-m-d') . '.captcha.txt'; /* use GMT date for filename */
3736728cfa6SSascha Leib		$logline = gmdate('Y-m-d H:i:s'); /* use GMT time for log entries */
3746728cfa6SSascha Leib		foreach ($logArr as $tab) {
3756728cfa6SSascha Leib			$logline .= "\t" . $tab;
3766728cfa6SSascha Leib		};
3776728cfa6SSascha Leib
3786728cfa6SSascha Leib		/* write the log line to the file */
3796728cfa6SSascha Leib		$logfile = fopen($filename, 'a');
3806728cfa6SSascha Leib		if (!$logfile) die();
3816728cfa6SSascha Leib		if (fwrite($logfile, $logline . "\n") === false) {
3826728cfa6SSascha Leib			fclose($logfile);
3836728cfa6SSascha Leib			die();
3846728cfa6SSascha Leib		}
3856728cfa6SSascha Leib
3869b2115f1SSascha Leib		// in case of errors, write the cookie data to the log:
3879b2115f1SSascha Leib		if (!$cookieVal) {
3880cfc0c5dSSascha Leib			$logline =  print_r($_COOKIE, true);
3899b2115f1SSascha Leib			if (fwrite($logfile, $logline . "\n") === false) {
3909b2115f1SSascha Leib				fclose($logfile);
3919b2115f1SSascha Leib				die();
3929b2115f1SSascha Leib			}
3939b2115f1SSascha Leib		}
3949b2115f1SSascha Leib
3959b2115f1SSascha Leib		/* Done. close the file. */
3966728cfa6SSascha Leib		fclose($logfile);
3976728cfa6SSascha Leib	}
3986728cfa6SSascha Leib
3992c641262SSascha Leib	// check if the visitor's IP is on a whitelist:
4002c641262SSascha Leib	private function captchaWhitelisted() {
4012c641262SSascha Leib
4022c641262SSascha Leib		// normalise IP address:
4032c641262SSascha Leib		$ip = inet_pton($_SERVER['REMOTE_ADDR']);
4042c641262SSascha Leib
4052c641262SSascha Leib		// find which file to open:
4062c641262SSascha Leib		$prefixes = ['user', 'default'];
4072c641262SSascha Leib		foreach ($prefixes as $pre) {
4082c641262SSascha Leib			$filename = __DIR__ .'/config/' . $pre . '-whitelist.txt';
4092c641262SSascha Leib			if (file_exists($filename)) {
4102c641262SSascha Leib				break;
4112c641262SSascha Leib			}
4122c641262SSascha Leib		}
4132c641262SSascha Leib
4142c641262SSascha Leib		if (file_exists($filename)) {
4152c641262SSascha Leib			$lines = file($filename, FILE_SKIP_EMPTY_LINES);
4162c641262SSascha Leib			foreach ($lines as $line) {
4172c641262SSascha Leib				if (trim($line) !== '' && !str_starts_with($line, '#')) {
4182c641262SSascha Leib					$col = explode("\t", $line);
4192c641262SSascha Leib					if (count($col) >= 2) {
4202c641262SSascha Leib						$from = inet_pton($col[0]);
4212c641262SSascha Leib						$to = inet_pton($col[1]);
4222c641262SSascha Leib
4232c641262SSascha Leib						if ($ip >= $from && $ip <= $to) {
424d49ab213SSascha Leib							return true; /* IP whitelisted */
4252c641262SSascha Leib						}
4262c641262SSascha Leib					}
4272c641262SSascha Leib				}
4282c641262SSascha Leib			}
4292c641262SSascha Leib		}
430d49ab213SSascha Leib		return false; /* IP not found in whitelist */
4312c641262SSascha Leib	}
4322c641262SSascha Leib
4337dae1c4dSSascha Leib	// inserts a static text content in place of the actual page content:
434393de67cSSascha Leib	private function insertLoremIpsum() {
43512993035SSascha Leib
436393de67cSSascha Leib		echo '<div class="level1">' . NL;
437393de67cSSascha 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;
438393de67cSSascha 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;
439393de67cSSascha Leib		echo '</div>' . NL;
440393de67cSSascha Leib
44112993035SSascha Leib	}
44212993035SSascha Leib
44312993035SSascha Leib	/* Generates a few paragraphs of Dada text to show instead of the article content */
444f5f4ca13SSascha Leib	private function insertDadaFiller() {
445f5f4ca13SSascha Leib
44612993035SSascha Leib		global $conf;
44712993035SSascha Leib		global $TOC;
44812993035SSascha Leib		global $ID;
449f5f4ca13SSascha Leib
45012993035SSascha Leib		// list of languages to search for the wordlist
45112993035SSascha Leib		$langs = array_unique([$conf['lang'], 'la']);
452f5f4ca13SSascha Leib
45312993035SSascha Leib		// find path to the first available wordlist:
45412993035SSascha Leib		foreach ($langs as $lang) {
45512993035SSascha Leib			$filename = __DIR__ .'/lang/' . $lang . '/wordlist.txt'; /* language-specific wordlist */
45612993035SSascha Leib			if (file_exists($filename)) {
45712993035SSascha Leib				break;
45812993035SSascha Leib			}
45912993035SSascha Leib		}
460f5f4ca13SSascha Leib
46112993035SSascha Leib		// load the wordlist file:
46212993035SSascha Leib		if (file_exists($filename)) {
46312993035SSascha Leib			$words = array();
46412993035SSascha Leib			$totalWeight = 0;
46512993035SSascha Leib			$lines = file($filename, FILE_SKIP_EMPTY_LINES);
46612993035SSascha Leib			foreach ($lines as $line) {
46712993035SSascha Leib				$arr = explode("\t", $line);
46812993035SSascha Leib				$arr[1] = ( count($arr) > 1 ? (int) trim($arr[1]) : 1 );
46912993035SSascha Leib				$totalWeight += (int) $arr[1];
47012993035SSascha Leib				array_push($words, $arr);
47112993035SSascha Leib			}
47212993035SSascha Leib		} else {
47312993035SSascha Leib			echo '<script> console.log("Can’t generate filler text: wordlist file not found!"); </script>';
47412993035SSascha Leib			return;
47512993035SSascha Leib		}
476f5f4ca13SSascha Leib
47712993035SSascha Leib		// If a TOC exists, use it for the headlines:
47812993035SSascha Leib		if(is_array($TOC)) {
47912993035SSascha Leib			$toc = $TOC;
48012993035SSascha Leib		} else {
48112993035SSascha Leib			$meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
48212993035SSascha Leib			//$tocok = (isset($meta['internal']['toc']) ? $meta['internal']['toc'] : $tocok = true);
48312993035SSascha Leib			$toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null;
48412993035SSascha Leib		}
48512993035SSascha Leib		if (!$toc) { // no TOC, generate my own:
48612993035SSascha Leib			$hlCount = mt_rand(0, (int) $conf['tocminheads']);
48712993035SSascha Leib			$toc = array();
48812993035SSascha Leib			for ($i=0; $i<$hlCount; $i++) {
48912993035SSascha Leib				array_push($toc, $this->dadaMakeHeadline($words, $totalWeight)); // $toc
49012993035SSascha Leib			}
49112993035SSascha Leib		}
49212993035SSascha Leib
49312993035SSascha Leib		// if H1 heading is not in the TOC, add a chappeau section:
49412993035SSascha Leib		$chapeauCount = mt_rand(1, 3);
49512993035SSascha Leib		if ((int) $conf['toptoclevel'] > 1) {
49612993035SSascha Leib			echo "<div class=\"level1\">\n";
49712993035SSascha Leib			for ($i=0; $i<$chapeauCount; $i++) {
49812993035SSascha Leib				echo $this->dadaMakeParagraph($words, $totalWeight);
49912993035SSascha Leib			}
50012993035SSascha Leib			echo "</div>\n";
50112993035SSascha Leib		}
50212993035SSascha Leib
50312993035SSascha Leib		//  text sections for each sub-headline:
50412993035SSascha Leib		foreach ($toc as $hl) {
50512993035SSascha Leib			echo $this->dadaMakeSection($words, $totalWeight, $hl);
50612993035SSascha Leib		}
50712993035SSascha Leib	}
50812993035SSascha Leib
50912993035SSascha Leib	private function dadaMakeSection($words, $totalWeight, $hl) {
51012993035SSascha Leib
51112993035SSascha Leib		global $conf;
51212993035SSascha Leib
51312993035SSascha Leib		// how many paragraphs?
51412993035SSascha Leib		$paragraphCount = mt_rand(1, 4);
51512993035SSascha Leib
51612993035SSascha Leib		// section level
51712993035SSascha Leib		$topTocLevel = (int) $conf['toptoclevel'];
51812993035SSascha Leib		$secLevel = $hl['level'] + 1;;
51912993035SSascha Leib
52012993035SSascha Leib		// return value:
52112993035SSascha Leib		$sec = "";
52212993035SSascha Leib
52312993035SSascha Leib		// make a headline:
52412993035SSascha Leib		if ($topTocLevel > 1 || $secLevel > 1) {
52512993035SSascha Leib			$sec .= "<h{$secLevel} id=\"{$hl['hid']}\">{$hl['title']}</h{$secLevel}>\n";
52612993035SSascha Leib		}
52712993035SSascha Leib
52812993035SSascha Leib		// add the paragraphs:
52912993035SSascha Leib		$sec .= "<div class=\"level{$secLevel}\">\n";
53012993035SSascha Leib		for ($i=0; $i<$paragraphCount; $i++) {
53112993035SSascha Leib			$sec .= $this->dadaMakeParagraph($words, $totalWeight);
53212993035SSascha Leib		}
53312993035SSascha Leib		$sec .= "</div>\n";
53412993035SSascha Leib
53512993035SSascha Leib		return $sec;
53612993035SSascha Leib	}
53712993035SSascha Leib
53812993035SSascha Leib	private function dadaMakeHeadline($words, $totalWeight) {
53912993035SSascha Leib
54012993035SSascha Leib		// how many words to generate?
54112993035SSascha Leib		$wordCount = mt_rand(2, 5);
54212993035SSascha Leib
54312993035SSascha Leib		// function returns an array:
54412993035SSascha Leib		$r = Array();
54512993035SSascha Leib
54612993035SSascha Leib		// generate the headline:
54712993035SSascha Leib		$hlArr = array();
54812993035SSascha Leib		for ($i=0; $i<$wordCount; $i++) {
54912993035SSascha Leib			array_push($hlArr, $this->dadaSelectRandomWord($words, $totalWeight));
55012993035SSascha Leib		}
55112993035SSascha Leib
55212993035SSascha Leib		$r['title'] =  ucfirst(implode(' ', $hlArr));
55312993035SSascha Leib
55412993035SSascha Leib		$r['hid'] = preg_replace('/[^\w\d\-]+/i', '_', strtolower($r['title']));
55512993035SSascha Leib		$r['type'] = 'ul'; // always ul!
55612993035SSascha Leib		$r['level'] = 1; // always level 1 for now
55712993035SSascha Leib
55812993035SSascha Leib		return $r;
55912993035SSascha Leib	}
56012993035SSascha Leib
56112993035SSascha Leib	private function dadaMakeParagraph($words, $totalWeight) {
56212993035SSascha Leib
56312993035SSascha Leib		// how many words to generate?
56412993035SSascha Leib		$sentenceCount = mt_rand(2, 5);
56512993035SSascha Leib
56612993035SSascha Leib		$paragraph = array();
56712993035SSascha Leib		for ($i=0; $i<$sentenceCount; $i++) {
56812993035SSascha Leib			array_push($paragraph, $this->dadaMakeSentence($words, $totalWeight));
56912993035SSascha Leib		}
57012993035SSascha Leib
57112993035SSascha Leib		return "<p>\n" . implode(' ', $paragraph) . "\n</p>\n";
57212993035SSascha Leib
57312993035SSascha Leib	}
57412993035SSascha Leib
57512993035SSascha Leib	private function dadaMakeSentence($words, $totalWeight) {
57612993035SSascha Leib
57712993035SSascha Leib		// how many words to generate?
57812993035SSascha Leib		$wordCount = mt_rand(4, 20);
57912993035SSascha Leib
58012993035SSascha Leib		// generate the sentence:
58112993035SSascha Leib		$sentence = array();
58212993035SSascha Leib		for ($i=0; $i<$wordCount; $i++) {
58312993035SSascha Leib			array_push($sentence, $this->dadaSelectRandomWord($words, $totalWeight));
58412993035SSascha Leib		}
58512993035SSascha Leib
58612993035SSascha Leib		return ucfirst(implode(' ', $sentence)) . '.';
58712993035SSascha Leib
58812993035SSascha Leib	}
58912993035SSascha Leib
59012993035SSascha Leib	private function dadaSelectRandomWord($list, $totalWeight) {
59112993035SSascha Leib
59212993035SSascha Leib		// get a random selection:
59312993035SSascha Leib		$rand = mt_rand(0, $totalWeight);
59412993035SSascha Leib
59512993035SSascha Leib		// match the selection to the weighted list:
59612993035SSascha Leib		$cumulativeWeight = 0;
59712993035SSascha Leib		for ($i=0; $i<count($list); $i++) {
59812993035SSascha Leib			$cumulativeWeight += $list[$i][1];
59912993035SSascha Leib			if ($cumulativeWeight >= $rand) {
60012993035SSascha Leib				return $list[$i][0];
60112993035SSascha Leib			}
60212993035SSascha Leib		}
60312993035SSascha Leib		return '***';
604f5f4ca13SSascha Leib	}
605f5f4ca13SSascha Leib
6066980370bSSascha Leib}
607