<?php
/**
 * #  ergol-http
 * 
 * Gemini capsule server through http.
 * 
 * https://codeberg.org/adele.work/ergol-http
 * 
 * Version 0.4.1
 * 
 * ## Copyright 2021 Adële
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the 
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 */

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

require(__DIR__.'/config.php');

// Loading config...
$conf_filename = @realpath(CONFIG_PATH);

$conf_content = file_get_contents($conf_filename);
if($conf_content===false)
	die("Unable to open file ".$conf_filename."\n");

if(CONFIG_TYPE === "ergol") {
	$conf_content = preg_replace('/[\x00-\x1F\x80-\xFF]/', '',$conf_content);

	$conf = json_decode($conf_content);
	if($conf===null)
		die("Unable to parse ".$conf_filename." : ".json_last_error_msg()."\n");
}
else if(CONFIG_TYPE === "gemserv") {
	//config to array of line
	$gsConfigArray = explode("\n", $conf_content);

	//replace all [[server]] by [server_X] (toml -> ini)
	$nb = 0;
	foreach($gsConfigArray as &$line) {
		if($line === "[[server]]") {
			$line = "[server_".$nb."]";
			++$nb;
		}
	}

	//reassemble file and parse as array
	$gsConfig = implode("\n", $gsConfigArray);
	$ini = parse_ini_string($gsConfig, true);

	//create temp array to contain config and put general config values in it
	$configTemp = array();
	if(array_key_exists('port', $ini)) {
		$configTemp["port"] = (int) $ini["port"];
	}
	else {
		die('Unable to process '.CONFIG_PATH.': Missing value "port"');
	}

	//process for each capsule, from server_0 to server_$nb-1, put it in an array
	$capsules = array();
	for($i = 0; $i < $nb; ++$i) {
		$key = "server_".$i;

		//check that all required fields are present
		if(array_key_exists('dir', $ini[$key]) && array_key_exists('hostname', $ini[$key]) && array_key_exists('hostname', $ini[$key])) {
			//check absolute path
			if($ini[$key]["dir"][0] == "/") {
				$folder = $ini[$key]["dir"];
			}
			else {
				$folder = "{here}/".$ini[$key]["dir"];
			}
			$capsules[$ini[$key]["hostname"]] = (object) array(
				"folder" => $folder,
				"lang" => $ini[$key]["lang"],
				"lang_regex" => "/\.([a-z][a-z])\./",
				"auto_index_ext" => array(".gmi"),
				"redirect" => false
			);
		}
	}
	//convert capsules array to stdObj -> as in json_decode, and put it in config
	$configTemp["capsules"] = (object) $capsules;

	//convert array to stdObj -> as in json_decode
	$conf = (object) $configTemp;
}
else {
	die("Unknown config type: ".CONFIG_TYPE."\n");
}

foreach($conf->capsules as $hostname => $capsule)
{
	if(empty($conf->capsules->$hostname->redirect))
	{
		$conf->capsules->$hostname->folder = str_replace("{here}",dirname($conf_filename),$capsule->folder);
	}
	else
	{
		unset($conf->capsules->$hostname->folder);
	}
}

if(strpos($_SERVER['HTTP_HOST'],':')!==false)
	$capsule = strtolower(substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'],':')));
else
	$capsule = strtolower($_SERVER['HTTP_HOST']);

$response = false;
$response_code = 0;
$body = false;

if(isset($_GET['qx']))
{
	$response = "OK";
	$body = "# You are following a Gemini link to another server

You can't access all the Geminispace with this proxy. If you want to follow this link, you have to install ans use a Gemini client.

You asked to follow :
```gemini-url
".urldecode($_GET['qx'])."
```";
	$mime="text/html";
	$body=gmi2html($capsule, $body, 'en', $_GET['qx'], '');
}

if(isset($_GET['q']))
	$q = $_GET['q'];
else
	$q = $_SERVER['REQUEST_URI'];

if($response === false && !isset($conf->capsules->$capsule))
{
	$response = "HTTP/1.1 400 BAD REQUEST";
	$response_code = 0;
}

if($response === false && strpos(str_replace("\\",'/',rawurldecode($q)),'/..')!==false)
{
	$response = "HTTP/1.1 400 BAD REQUEST";
	$response_code = 0;
}

if(!empty($conf->capsules->$capsule->redirect))
{
	// redirect to another capsule
	$response = "Location: ".str_replace('gemini://','http://',$conf->capsules->$capsule->redirect.$q);
	$response_code = 302;
}
elseif($response === false)
{
	// search requested file
	$filename = $conf->capsules->$capsule->folder.rawurldecode($q);
	$lang = $conf->capsules->$capsule->lang;
	if(!empty($conf->capsules->$capsule->lang_regex))
	{
		// search lang code in requested path (ex: file.fr.gmi)
		preg_match($conf->capsules->$capsule->lang_regex, rawurldecode($q), $matches);
		if(isset($matches[1]))
			$lang = strtolower($matches[1]);
	}
	// search favicon
	$favicon = @file_get_contents($conf->capsules->$capsule->folder.'/favicon.txt');
	$favicon = mb_substr(trim($favicon),0,1);
}

if($response === false && $q==='/favicon.ico' && !empty($favicon))
{
	// generate favicon
	$image = new Imagick();
	$draw = new ImagickDraw();
	$pixel = new ImagickPixel( 'white' );
	$image->newImage(128, 128, $pixel);
	$draw->setFont('TwitterColorEmoji-SVGinOT.ttf');
	$draw->setFontSize( 120 );
	$draw->setFillColor('#999');
	$image->annotateImage($draw, 3, 107, 0, $favicon);
	$image->annotateImage($draw, 4, 106, 0, $favicon);
	$image->annotateImage($draw, 5, 107, 0, $favicon);
	$image->annotateImage($draw, 2, 108, 0, $favicon);
	$draw->setFillColor('#666');
	$image->annotateImage($draw, 6, 108, 0, $favicon);
	$image->annotateImage($draw, 3, 109, 0, $favicon);
	$image->annotateImage($draw, 4, 110, 0, $favicon);
	$image->annotateImage($draw, 5, 109, 0, $favicon);
	$draw->setFillColor('#333');
	$image->annotateImage($draw, 4, 108, 0, $favicon);
	$image->setImageFormat('png');
	header('Content-type: image/png');
	echo $image;
	exit;
}

if($response === false && file_exists($filename))
{
	if($response === false && is_file($filename))
	{
		
		$mime = mime_content_type($filename);
		if($mime == "text/plain")
		{
			if(substr($q,-4)=='.gmi')
				$mime = "text/gemini";
			elseif(substr($q,-3)=='.md')
				$mime = "text/markdown";
			elseif(substr($q,-4)=='.html')
				$mime = "text/html";
		}
		$response = "OK";
		$body = file_get_contents($filename);
		if($mime=="text/gemini")
		{
			$mime="text/html";
			$body=gmi2html($capsule, $body, $lang,
				'gemini://'.$capsule.($conf->port==1965?'':(':'.$conf->port)).$q,
				$favicon);
		}
	}
	
	if($response === false && is_dir($filename))
	{
		// if path is a directory name redirect into it
		if(substr($filename,-1)!='/')
		{
			$response = "Location: ".$q."/";
			$response_code = 302;
		}
		else
		{
			$mime = "text/html";
			if(file_exists($filename.'/index.gmi'))
			{
				// open default file index.gmi
				$response = "OK";
				$filename = $filename.'/index.gmi';
				$body = file_get_contents($filename);
				$body = gmi2html($capsule, $body, $lang,
					'gemini://'.$capsule.($conf->port==1965?'':(':'.$conf->port)).$q,
					$favicon);
			}
			elseif(is_array($conf->capsules->$capsule->auto_index_ext))
			{
				// build auto index
				$response = "OK";
				$body = "# ".$capsule." ".basename($filename)."\r\n";
				$body .= "=> ../ [..]\r\n";
				// three blocks
				$items_dir=array(); // sub directories
				$items_gmi=array(); // gmi file chronogically desc
				$items_oth=array(); // other files
				$d = dir($filename);
				while (false !== ($entry = $d->read()))
				{
					if(substr($entry,0,1)=='.')
					{
						// dir itself
						continue;
					}
					if(is_dir($filename.'/'.$entry) &&
						!in_array('/', $conf->capsules->$capsule->auto_index_ext))
					{
						// folder ext "/" not in auto_index conf
						continue;
					}
					if(is_file($filename.'/'.$entry) &&
						!in_array(substr($entry,strrpos($entry,'.')), $conf->capsules->$capsule->auto_index_ext))
					{
						// ext not in auto_index conf
						continue;
					}						$link_name = $entry;
					if(substr($entry,-4)=='.gmi')
					{
						// build feed for subscriptions for .gmi files,
						// adding date YYYY-MM-DD in link if not file name
						// see specs gemini://gemini.circumlunar.space/docs/companion/subscription.gmi
						$entry_name = str_replace('_',' ',substr($entry,0,-4));
						if(!preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])\s$/",substr($entry_name,0,11)))
							$link_name = date("Y-m-d", filemtime($filename.'/'.$entry))." ".$entry_name;
						else
							$link_name = " ".$entry_name;
						$items_gmi[$link_name." ".$entry] = "=> ".rawurlencode($entry)." ".$link_name;
					}
					elseif(is_dir($filename.'/'.$entry))
					{
						// sub directory
						$link_name = "[".$entry."]";
						$items_dir[$entry] = "=> ".rawurlencode($entry)."/ ".$link_name;
					}
					else
					{
						// other file ext
						$items_oth[$entry] = "=> ".rawurlencode($entry)." ".$link_name;
					}
				}
				$d->close();
				ksort($items_dir);
				krsort($items_gmi);
				ksort($items_oth);
				if(count($items_dir)>0)
					$body .= implode("\r\n", $items_dir)."\r\n";
				if(count($items_gmi)>0)
					$body .= implode("\r\n", $items_gmi)."\r\n";
				if(count($items_oth)>0)
					$body .= implode("\r\n", $items_oth)."\r\n";
				$body = gmi2html($capsule, $body, $lang,
					'gemini://'.$capsule.($conf->port==1965?'':(':'.$conf->port)).$q,
					$favicon);
			}
		}
	}
}

if($response === false)
{
	$response = "HTTP/1.1 404 NOT FOUND";
	$response_code = 0;
}

if($response != "OK")
{
	header($response, true, $response_code);
	exit;
}

header("Content-Type: ".$mime, true);
header("Content-Length: ".strlen($body), true);
echo $body;
exit;


function gmi2html($capsule, $body, $lang, $urlgem, $favicon)
{
	if(isset($_SERVER['REQUEST_SCHEME'])) {
		$scheme = $_SERVER['REQUEST_SCHEME'];
	}
	else if(isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS'])) {
		$scheme = 'https';
	}
	else {
		$scheme = 'http';
	}
	$title='';
	$lines=array();
	$tocs=array();
	$lev1=0;
	$lev2=0;
	$lev3=0;
	$pre=false;
	$glines = explode("\n", $body);
	foreach($glines as $line)
	{
		if($pre && substr(trim($line, "\r\n"),0,3)!='```')
		{
			$lines[] = str_replace(array('&','<','>','"',"'"), array('&amp;','&lt;','&gt;','&quot;','&#39;'), $line);
			continue;
		}
		$line=trim($line, "\r\n");
		$prefix = explode(' ',substr($line,0,3),2);
		$prefix=$prefix[0];
		// if no space before titles
		if(substr($line,0,1)=='#')
			$prefix='#';
		if(substr($line,0,2)=='##')
			$prefix='##';
		if(substr($line,0,3)=='###')
			$prefix='###';
		if($prefix=="```")
		{
			if($pre)
				$lines[]='</pre>';
			else
				$lines[]='<pre title="'.htmlentities(substr($line,3)).'">';
			$pre=!$pre;
			continue;
		}
		if($prefix=="#" && empty($title))
			$title = trim(substr($line,2));
		switch($prefix)
		{
			case "#":
				$lev1++;
				$lev2=0;
				$lev3=0;
				$levid = $lev1;
				$lines[] = '<h1 id="'.$levid.'">'.trim(htmlentities(substr($line,1))).'</h1>';
				$tocs[] = '<li class="l1"><a href="#'.$levid.'">'.trim(htmlentities(substr($line,1))).'</a></li>';
				break;
			case "##":
				$lev2++;
				$lev3=0;
				$levid = $lev1.'-'.$lev2;
				$lines[] = '<h2 id="'.$levid.'">'.trim(htmlentities(substr($line,2))).'</h2>';
				$tocs[] = '<li class="l2"><a href="#'.$levid.'">'.trim(htmlentities(substr($line,2))).'</a></li>';
				break;
			case "###":
				$lev3++;
				$levid = $lev1.'-'.$lev2.'-'.$lev3;
				$lines[] = '<h3 id="'.$levid.'">'.trim(htmlentities(substr($line,3))).'</h3>';
				$tocs[] = '<li class="l3"><a href="#'.$levid.'">'.trim(htmlentities(substr($line,3))).'</a></li>';
				break;
			case ">":
				$lines[] = "<blockquote>".htmlentities(substr($line,2))."</blockquote>";
				break;
			case "*":
				$lines[] = "<li>".htmlentities(substr($line,2))."</li>";
				break;
			case "=>":
				$lines[]='<p>';
				$link = explode(' ', substr($line,3), 2);
				if(str_starts_with($link[0], 'gemini://'.$capsule)) {
					$lines[] = '<a href="'.str_replace('gemini://'.$capsule,$scheme.'://'.$_SERVER['HTTP_HOST'], $link[0]).'">'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1])."</a>";
				}
				else if(str_starts_with($link[0], 'gemini://')) {
					$lines[] = '<a href="/?qx='.urlencode($link[0]).'">'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1])."</a>";
				}
				else {
					$lines[] = '<a href="'.$link[0].'">'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1])."</a>";
				}
				if(strpos($link[0], '://')===false &&		 // relative image
				   in_array(strtolower(substr($link[0],-4)),array('.jpg','.png','.gif','jpeg','webp')) )
					$lines[] = ' 🖼️ <div class="inline-img"><img src="'.$link[0].'" alt="'.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1]).'" /></div>';
				$lines[]='</p>';
				break;
			default:
				$lines[] = "<p>".htmlentities($line)."</p>";
				break;
		}
	}
	$style = file_get_contents(__DIR__.'/style.css');
	ob_start();
	include "template.php";
	$html = ob_get_contents();
	ob_end_clean();
	return $html;
}
