diff --git a/README.md b/README.md index 666e714..fb9d39b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,16 @@ # ergol-http -Ergol companion to serve #gemini capsules through http/https. -It is a http wrapper written in php working with ergol gemini server or in standalone. \ No newline at end of file +Ergol companion to serve #gemini capsules through http/https. +It is a http wrapper written in php working with ergol gemini server or in standalone. + +## Copyright + +author : [Adële](https://adele.work/) + +ergol-http is under MIT/X Consortium License + +ergol-http uses and provides a copy of Twitter Emoji aka twemoji packaged in a TrueType font format. +[twemoji](https://twemoji.twitter.com/) is also published under [MIT license](http://opensource.org/licenses/MIT). + +Main repository on [Codeberg](https://codeberg.org/adele.work/ergol-http). + diff --git a/TwitterColorEmoji-SVGinOT.ttf b/TwitterColorEmoji-SVGinOT.ttf new file mode 100644 index 0000000..a8f829a Binary files /dev/null and b/TwitterColorEmoji-SVGinOT.ttf differ diff --git a/changelog.gmi b/changelog.gmi new file mode 100755 index 0000000..72070ee --- /dev/null +++ b/changelog.gmi @@ -0,0 +1,11 @@ +# Ergol changelog + +## Version 0.4 +2021-03-12 15:50:00 UTC +* shows relative image inline + +## Version 0.3 +2021-03-08 15:20:00 UTC +* reply to /favicon.ico request and generate a png with TwitterColorEmoji-SVGinOT.ttf + +=> ../ergol.gmi Ergol page diff --git a/config-sample.php b/config-sample.php new file mode 100644 index 0000000..a7799a5 --- /dev/null +++ b/config-sample.php @@ -0,0 +1,6 @@ +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($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) +{ + $title=''; + $lines=array(); + $pre=false; + $glines = explode("\n", $body); + foreach($glines as $line) + { + if($pre && substr(trim($line, "\r\n"),0,3)!='```') + { + $lines[] = str_replace(array('&','<','>','"',"'"), array('&','<','>','"','''), $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[]=''; + else + $lines[]='
';
+			$pre=!$pre;
+			continue;
+		}
+		if($prefix=="#" && empty($title))
+			$title = trim(substr($line,2));
+		switch($prefix)
+		{
+			case "#":
+				$lines[] = "

".htmlentities(substr($line,1))."

"; + break; + case "##": + $lines[] = "

".htmlentities(substr($line,2))."

"; + break; + case "###": + $lines[] = "

".htmlentities(substr($line,3))."

"; + break; + case ">": + $lines[] = "
".htmlentities(substr($line,2))."
"; + break; + case "*": + $lines[] = "
  • ".htmlentities(substr($line,2))."
  • "; + break; + case "=>": + $lines[]='

    '; + $link = explode(' ', substr($line,3), 2); + $lines[] = ''.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1]).""; + if(strpos($link[0], '://')===false && // relative image + in_array(strtolower(substr($link[0],-4)),array('.jpg','.png','.gif','jpeg','webp')) ) + $lines[] = ' 🖼️

    '.htmlentities(empty($link[1])?rawurldecode($link[0]):$link[1]).'
    '; + $lines[]='

    '; + break; + default: + $lines[] = "

    ".htmlentities($line)."

    "; + break; + } + } + $html = ' + + + + + '.htmlentities($title.' | '.$urlgem).' + + + + +
    + '.implode("\n",$lines).' +
    + + + '; + return $html; +} diff --git a/style.css b/style.css new file mode 100755 index 0000000..925f73b --- /dev/null +++ b/style.css @@ -0,0 +1,51 @@ +body { + margin: 0; + padding: 0; +} +div.main { + margin: 4em auto; + padding: 1em; + max-width: 72em; + border: 1px solid #ccc; + box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, .1);; +} +pre { + background: #eee; + padding: 1em; + overflow-x: auto; +} +li { + margin-left: 1em; +} +p { + margin: 0.5em 0; +} +blockquote { + background: #ffc; + padding: 1em; + margin: 0.5em; +} +div.inline-img img { + max-width: 100%; +} +div.gemini { + margin: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + padding: 0.5em 0; + font-family: monospace; + font-size: 120%; + background: #000; +} +div.gemini a { + color: #3f3; + text-decoration: none; + word-break: break-all; +} +div.gemini span { + color: #fff; + font-size: 150%; + margin-left: 1em; +}