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[] = "
'; + $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[] = ' 🖼️
'; + $lines[]=''; + break; + default: + $lines[] = "".htmlentities($line)."
"; + break; + } + } + $html = ' + + + + +