<commit_before><?php

error_reporting(0);

function escapeHtml($s) {
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

function escapeHtmlAttr($s) {
  if (strpos($s, '`') !== false && strpbrk($s, ' <>"\'') === false) { $s .= ' '; }
  return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

class CommentSection {
	private $baseDir    = '.comments';
	private $pathRegex  = '~.*\.html~';
	private $globalPath = 'global.rss.html';

	function addComment($path, $comment) {
		$this->append($this->openFile($path), $comment);
		$this->append($this->openFile($this->globalPath, false), $comment);
	}

	function getComments($path, $flat = false) {
		return $this->_getComments($this->openFile($path), $flat);
	}

	function getCommentsGlobal() {
		$block = $this->_getComments($this->openFile($this->globalPath, false), true);
		$block->title = '{comments.commentsTo}';
		return $block;
	}

	private function _getComments($file, $flat = false) {
		$lines = array_map('json_decode', array_map('trim', file($file)));
		$block = $lines[0];
		$cs = array();
		foreach (array_slice($lines, 1) as $c) {
			$cs[$c->id] = $c;
		}
		if (!$flat) {
			foreach (array_reverse($cs) as $c) {
				if (isset($c->replyTo) && $cs[$c->replyTo]) {
					if (!isset($cs[$c->replyTo]->replies)) $cs[$c->replyTo]->replies = array();
					array_unshift($cs[$c->replyTo]->replies, $c);
					unset($cs[$c->id]);
				}
			}
		}
		$block->comments = $cs;
		return $block;
	}

	private function openFile($path, $fetchTitle = true) {
		if ($path[0] === '/' || strpos($path, '..') !== false || !preg_match($this->pathRegex, $path))
			throw new \Exception("invalid path");

		$file = $this->baseDir.'/'.str_replace("/", "_", $path);
		if (!file_exists($file)) {
			if (!file_exists($this->baseDir)) {
				mkdir($this->baseDir);
			}
			$header = array('path' => $path, 'title' => $fetchTitle ? $this->getTitle($path) : null);
			$this->append($file, $header);
		}
		return $file;
	}

	private function getTitle($path) {
		$html = @file_get_contents($path);
		if ($html === false) {
			throw new \Exception("page '$path' cannot be found");
		}
		$dom = new \DOMDocument('1.0', 'UTF-8');
		$html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8");
		$ok = @ $dom->loadHTML($html);
		if ($ok === false) {
			throw new \Exception("page '$path' cannot be found");
		}
		$xpath = new \DOMXPath($dom);
		return $xpath->query('//h2')->item(0)->nodeValue;
	}

	private function append($file, $data) {
		file_put_contents($file, json_encode($data)."\n", FILE_APPEND | LOCK_EX);
	}
}

function mkText($txt) {
	return str_replace("\n", "<br/>", $txt);
}


try {

$commentSection = new CommentSection;
$requestUrl = $_SERVER['REQUEST_URI'];
$url = (string) $_GET['url'];

if (isset($_POST['text'])) {
	$comment = (object) array(
		'id'   => mt_rand(),
		'name' => (string) $_POST['name'],
		'mail' => (string) $_POST['mail'],
		'web'  => (string) $_POST['web'],
		'text' => (string) $_POST['text'],
		'date' => date("Y-m-d H:i:s", time()),
		'ip'   => (string) $_SERVER['REMOTE_ADDR'],
		'path' => $url,
	);

	if ($comment->text === '') throw new \Exception("Text field is required.");
	if (strlen($comment->name) > 60) throw new \Exception("Name is too long.");
	if (strlen($comment->mail) > 60) throw new \Exception("Mail is too long.");
	if (strlen($comment->web)  > 60) throw new \Exception("Web is too long.");
	if (strlen($comment->text) > 2000) throw new \Exception("Text is too long.");
	if ($comment->name === '') { $comment->name = 'Anonymous'; }

	if ($_POST['replyTo']) {
		$replyTo = (int) $_POST['replyTo'];
		$valid = false;
		foreach ($commentSection->getComments($url, true) as $c) {
			if ($c->id == $replyTo) $valid = true;
		}

		if (!$valid || $c->replyTo == $c->id) {
			throw new \Exception("Replying to invalid comment.");
		}

		$comment->replyTo = $replyTo;
	}

	$commentSection->addComment($url, $comment);
	setcookie('user', json_encode(array($comment->name, $comment->mail, $comment->web)), time()+3600*24*30);
	header('Location: '.$requestUrl);
	exit;

} elseif (isset($_GET['rss'])) {
	$block = $url ? $commentSection->getComments($url) : $commentSection->getCommentsGlobal();

	$rss = new \SimpleXMLElement('<rss version="2.0"></rss>');
	$rss->channel->title = $block->title;
	$rss->channel->link  = "{comments.baseUrl}/comments.php?url=".escapeHtmlAttr($block->path);

	foreach ($block->comments as $c) {
		$item = $rss->channel->addChild("item");
		$item->title = "$block->title - $c->name";
		$item->description = $c->text;
		$item->guid  = "{comments.baseUrl}/comments.php?url=".escapeHtmlAttr(isset($c->path) ? $c->path : $block->path)."#".$c->id;
		$item->guid["isPermalink"] = "true";
		$item->pubDate = date(DATE_RSS, strtotime($c->date));
	}

	header("Content-Type: application/xml; charset=UTF-8");
	echo $rss->asXML();
	exit;

} else {
	$block = $commentSection->getComments($url);
	@list($name, $mail, $web) = isset($_COOKIE['user']) ? array_map('strval', json_decode((string)$_COOKIE['user'])) : array('', '', '');

	echo '{comments.prebody}';
	echo "{comments.commentsTo} <h2><a href='", escapeHtmlAttr($block->path) , "'>", escapeHtml($block->title), "</a></h2><br/></br/>";<commit_msg>comments - add simple ip and term blacklists<commit_after><?php

error_reporting(0);

function escapeHtml($s) {
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

function escapeHtmlAttr($s) {
  if (strpos($s, '`') !== false && strpbrk($s, ' <>"\'') === false) { $s .= ' '; }
  return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

class CommentSection {
	private $baseDir    = '.comments';
	private $pathRegex  = '~.*\.html~';
	private $globalPath = 'global.rss.html';

	function isSpam($comment) {
		$ipBlacklistFile   = $this->baseDir."/ip-blacklist";
		$termBlacklistFile = $this->baseDir."/term-blacklist";

		if (file_exists($ipBlacklistFile)) {
			$ips = array_map('trim', file($ipBlacklistFile));
			foreach ($ips as $ip) {
				if (substr($comment->ip, 0, strlen($ip)) === $ip) {
					return true;
				}
			}
		}

		if (file_exists($termBlacklistFile)) {
			$terms = array_map('trim', file($termBlacklistFile));
			foreach ($terms as $term) {
				if (strpos($comment->text, $term) !== false) {
					return true;
				}
			}
		}

		return false;
	}

	function addComment($path, $comment) {
		$this->append($this->openFile($path), $comment);
		$this->append($this->openFile($this->globalPath, false), $comment);
	}

	function getComments($path, $flat = false) {
		return $this->_getComments($this->openFile($path), $flat);
	}

	function getCommentsGlobal() {
		$block = $this->_getComments($this->openFile($this->globalPath, false), true);
		$block->title = '{comments.commentsTo}';
		return $block;
	}

	private function _getComments($file, $flat = false) {
		$lines = array_map('json_decode', array_map('trim', file($file)));
		$block = $lines[0];
		$cs = array();
		foreach (array_slice($lines, 1) as $c) {
			$cs[$c->id] = $c;
		}
		if (!$flat) {
			foreach (array_reverse($cs) as $c) {
				if (isset($c->replyTo) && $cs[$c->replyTo]) {
					if (!isset($cs[$c->replyTo]->replies)) $cs[$c->replyTo]->replies = array();
					array_unshift($cs[$c->replyTo]->replies, $c);
					unset($cs[$c->id]);
				}
			}
		}
		$block->comments = $cs;
		return $block;
	}

	private function openFile($path, $fetchTitle = true) {
		if ($path[0] === '/' || strpos($path, '..') !== false || !preg_match($this->pathRegex, $path))
			throw new \Exception("invalid path");

		$file = $this->baseDir.'/'.str_replace("/", "_", $path);
		if (!file_exists($file)) {
			if (!file_exists($this->baseDir)) {
				mkdir($this->baseDir);
			}
			$header = array('path' => $path, 'title' => $fetchTitle ? $this->getTitle($path) : null);
			$this->append($file, $header);
		}
		return $file;
	}

	private function getTitle($path) {
		$html = @file_get_contents($path);
		if ($html === false) {
			throw new \Exception("page '$path' cannot be found");
		}
		$dom = new \DOMDocument('1.0', 'UTF-8');
		$html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8");
		$ok = @ $dom->loadHTML($html);
		if ($ok === false) {
			throw new \Exception("page '$path' cannot be found");
		}
		$xpath = new \DOMXPath($dom);
		return $xpath->query('//h2')->item(0)->nodeValue;
	}

	private function append($file, $data) {
		file_put_contents($file, json_encode($data)."\n", FILE_APPEND | LOCK_EX);
	}
}

function mkText($txt) {
	return str_replace("\n", "<br/>", $txt);
}


try {

$commentSection = new CommentSection;
$requestUrl = $_SERVER['REQUEST_URI'];
$url = (string) $_GET['url'];

if (isset($_POST['text'])) {
	$comment = (object) array(
		'id'   => mt_rand(),
		'name' => (string) $_POST['name'],
		'mail' => (string) $_POST['mail'],
		'web'  => (string) $_POST['web'],
		'text' => (string) $_POST['text'],
		'date' => date("Y-m-d H:i:s", time()),
		'ip'   => (string) $_SERVER['REMOTE_ADDR'],
		'path' => $url,
	);

	if ($comment->text === '') throw new \Exception("Text field is required.");
	if (strlen($comment->name) > 60) throw new \Exception("Name is too long.");
	if (strlen($comment->mail) > 60) throw new \Exception("Mail is too long.");
	if (strlen($comment->web)  > 60) throw new \Exception("Web is too long.");
	if (strlen($comment->text) > 2000) throw new \Exception("Text is too long.");
	if ($comment->name === '') { $comment->name = 'Anonymous'; }

	if ($_POST['replyTo']) {
		$replyTo = (int) $_POST['replyTo'];
		$valid = false;
		foreach ($commentSection->getComments($url, true) as $c) {
			if ($c->id == $replyTo) $valid = true;
		}

		if (!$valid || $c->replyTo == $c->id) {
			throw new \Exception("Replying to invalid comment.");
		}

		$comment->replyTo = $replyTo;
	}

	if ($commentSection->isSpam($comment)) {
		exit;
	}
	$commentSection->addComment($url, $comment);
	setcookie('user', json_encode(array($comment->name, $comment->mail, $comment->web)), time()+3600*24*30);
	header('Location: '.$requestUrl);
	exit;

} elseif (isset($_GET['rss'])) {
	$block = $url ? $commentSection->getComments($url) : $commentSection->getCommentsGlobal();

	$rss = new \SimpleXMLElement('<rss version="2.0"></rss>');
	$rss->channel->title = $block->title;
	$rss->channel->link  = "{comments.baseUrl}/comments.php?url=".escapeHtmlAttr($block->path);

	foreach ($block->comments as $c) {
		$item = $rss->channel->addChild("item");
		$item->title = "$block->title - $c->name";
		$item->description = $c->text;
		$item->guid  = "{comments.baseUrl}/comments.php?url=".escapeHtmlAttr(isset($c->path) ? $c->path : $block->path)."#".$c->id;
		$item->guid["isPermalink"] = "true";
		$item->pubDate = date(DATE_RSS, strtotime($c->date));
	}

	header("Content-Type: application/xml; charset=UTF-8");
	echo $rss->asXML();
	exit;

} else {
	$block = $commentSection->getComments($url);
	@list($name, $mail, $web) = isset($_COOKIE['user']) ? array_map('strval', json_decode((string)$_COOKIE['user'])) : array('', '', '');

	echo '{comments.prebody}';
	echo "{comments.commentsTo} <h2><a href='", escapeHtmlAttr($block->path) , "'>", escapeHtml($block->title), "</a></h2><br/></br/>";
{'fraction_non_alphanumeric': 0.23774576424753102, 'fraction_numerical': 0.006432907492978164, 'mean_word_length': 4.507984031936128, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 208, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '705465', 'n_tokens_mistral': 4305, 'n_tokens_neox': 3983, 'n_words': 785}