Adds the url path

* The /htmgem directory can be renamed and moved.
* Everything works without URL rewriting enabled.
This commit is contained in:
Christophe HENRY 2021-04-11 21:35:55 +02:00
parent a9fb49802a
commit c11af12551
6 changed files with 276 additions and 93 deletions

View File

@ -2,7 +2,7 @@ html {
font-family: sans-serif; font-family: sans-serif;
} }
body { #gmi {
max-width: 1024px; max-width: 1024px;
margin: auto; margin: auto;
margin-top: 0.5em; margin-top: 0.5em;
@ -33,13 +33,26 @@ a {
text-decoration: none; text-decoration: none;
} }
a { .menu:nth-of-type(1) .menu-line {
text-decoration: none; text-align: left;
}
.menu:nth-of-type(3) .menu-line {
text-align: right;
}
.menu a, .menu a:visited {
#color: #888;
}
.menu a:hover {
#color: #000;
}
.menu hr {
border: 1px solid lightgrey;
} }
@media only screen and (max-width: 1024px) { @media only screen and (max-width: 1024px) {
body { body {
margin: 3rem; margin: 0.5rem 3rem;
} }
h1 { h1 {
font-size: 4rem; font-size: 4rem;
@ -53,4 +66,10 @@ a {
p, pre, ul, blockquote { p, pre, ul, blockquote {
font-size: 2.6rem; font-size: 2.6rem;
} }
.menu {
font-size: 2rem;
}
.menu hr {
border: 1px solid gray;
}
} }

View File

@ -17,47 +17,66 @@ blockquote {
padding: 1rem; padding: 1rem;
} }
a { .menu a, .menu a:visited {
margin: -1.35rem; color: #888;
}
.menu a:hover {
color: #000;
}
.menu hr {
color: white;
}
#gmi a {
margin: -0.7rem;
color:#820; color:#820;
} }
a:before { #gmi a:before {
content: "🔗 "; content: "🔗 ";
} }
a:visited { #gmi a:visited {
color: #868; color: #868;
} }
a.local:before { #gmi a.local:before {
content: "🛩️ "; content: "🛩️ ";
font-weight: bold; font-weight: bold;
} }
a.gemini:before { #gmi a.gemini:before {
content: "🚀 "; content: "🚀 ";
} }
a.gopher:before { #gmi a.gopher:before {
content: "📜 "; content: "📜 ";
} }
a.https:before { #gmi a.https:before {
content: "🕸️ "; content: "🕸️ ";
font-weight: bolder; font-weight: bolder;
} }
a.http:before { #gmi a.http:before {
content: "🕸️ "; content: "🕸️ ";
font-weight: lighter; font-weight: lighter;
} }
a.mumble:before { #gmi a.mumble:before {
content: "🎤 "; content: "🎤 ";
} }
a.mailto:before { #gmi a.mailto:before {
content: "✉️ "; content: "✉️ ";
} }
@media only screen and (max-width: 1024px) {
#gmi a {
margin: -2.9rem;
}
}

View File

@ -4,7 +4,11 @@ require_once "lib-htmgem.inc.php";
require_once "lib-html.inc.php"; require_once "lib-html.inc.php";
require_once "lib-io.inc.php"; require_once "lib-io.inc.php";
# The url argument is always absolute compared to the document root. $documentRoot = $_SERVER['DOCUMENT_ROOT'];
$scheme = (@$_SERVER['REQUEST_SCHEME']??"http")."://";
$domain = $_SERVER['HTTP_HOST'];
$php_self = $_SERVER['PHP_SELF']; // by default: /htmgem/index.php
$php_self_dir = dirname($php_self);
$url = @$_REQUEST["url"]; $url = @$_REQUEST["url"];
$urlRewriting = @$_REQUEST["rw"]=="1"; $urlRewriting = @$_REQUEST["rw"]=="1";
@ -18,18 +22,21 @@ $urlRewriting = @$_REQUEST["rw"]=="1";
if (empty($url)) { if (empty($url)) {
if (!file_exists("index.gmi")) { if (!file_exists("index.gmi")) {
http_response_code(403); http_response_code(403);
die("<!-- index.gmi missing -->"); } else {
$gt_html = new \htmgem\GemTextTranslate_html(file_get_contents("index.gmi"), true, "$php_self?url=", $php_self_dir);
if (empty($gt_html->getCss)) $gt_html->addCss($php_self_dir."/css/htmgem.css");
// No URL Rewritting assumed
echo \htmgem\html\getHtmlWithMenu($gt_html, $scheme, $domain, $php_self, "$php_self?url=");
} }
$gt_html = new \htmgem\GemTextTranslate_html(@file_get_contents("index.gmi"), true, "/htmgem");
echo \htmgem\html\getFullHtml($gt_html);
exit(); exit();
} }
$documentRoot = $_SERVER['DOCUMENT_ROOT']; $url = \htmgem\resolve_path(
// Some webservers (Apache) don't add the slash
/** // while others (Nginx) do…
* Provides index.gmi if no page given ( $url[0] == "/" ? "" : "/" ) . $url
*/ );
if (!preg_match("/\.gmi$/", $url)) { if (!preg_match("/\.gmi$/", $url)) {
if ($url[-1] == "/") if ($url[-1] == "/")
$url = $url."index.gmi"; $url = $url."index.gmi";
@ -55,9 +62,13 @@ switch(true) {
if ($go404) { if ($go404) {
error_log("HtmGem: 404 $url $filePath"); error_log("HtmGem: 404 $url $filePath");
http_response_code(404); http_response_code(404);
$page404 = \htmgem\html\get404GmiPage("Page not found", $url); $page404 = \htmgem\html\get404GmiPage($url);
$gt_html = new \htmgem\GemTextTranslate_html($page404); $gt_html = new \htmgem\GemTextTranslate_html($page404);
echo \htmgem\html\getFullHtml($gt_html); if (empty($gt_html->getCss)) $gt_html->addCss($php_self_dir."/css/htmgem.css");
if ($urlRewriting)
echo \htmgem\html\getHtmlWithMenu($gt_html, $scheme, $domain, $url);
else
echo \htmgem\html\getHtmlWithMenu($gt_html, $scheme, $domain, $url, "$php_self?url=");
exit(); exit();
} }
@ -101,12 +112,12 @@ EOL;
} }
if ($urlRewriting) if ($urlRewriting)
$baseUrl = null; $gt_html = new \htmgem\GemTextTranslate_html($fileContents, $gt_htmlextDecoration);
else else
$baseUrl = dirname($url); $gt_html = new \htmgem\GemTextTranslate_html($fileContents, $gt_htmlextDecoration, "$php_self?url=", dirname($url));
$gt_html = new \htmgem\GemTextTranslate_html($fileContents, $gt_htmlextDecoration, $baseUrl);
if ("none" == $style) { if ("none" == $style) {
$gt_html->addCss(""); #$gt_html->addCss("");
} elseif ("/" == @$style[0]) { } elseif ("/" == @$style[0]) {
$gt_html->addCss($style); $gt_html->addCss($style);
} elseif (empty($style)) { } elseif (empty($style)) {
@ -119,9 +130,13 @@ if ("none" == $style) {
$gt_html->addCss($localCss); $gt_html->addCss($localCss);
} }
} else { #TODO: regex check for $style } else { #TODO: regex check for $style
$gt_html->addCss("/htmgem/css/$style.css"); $gt_html->addCss("$php_self_dir/css/$style.css");
} }
if (empty($gt_html->getCss)) $gt_html->addCss($php_self_dir."/css/htmgem.css");
echo \htmgem\html\getFullHtml($gt_html); if ($urlRewriting)
echo \htmgem\html\getHtmlWithMenu($gt_html, $scheme, $domain, $url);
else
echo \htmgem\html\getHtmlWithMenu($gt_html, $scheme, $domain, $url, "$php_self?url=");
?> ?>

View File

@ -5,6 +5,55 @@ namespace htmgem;
mb_internal_encoding("UTF-8"); mb_internal_encoding("UTF-8");
mb_regex_encoding("UTF-8"); mb_regex_encoding("UTF-8");
/**
* Resolve $path interpretating / . and ..
* @param $path str
* @returns "/" if .. goes above the limit
*/
function resolve_path($path) {
if (empty($path)) return "";
$absolute = "/"==$path[0];
$parts = array_filter(explode("/", $path), 'strlen');
$chuncks = array();
foreach ($parts as $part) {
if ('.' == $part) continue;
if ('..' == $part) {
if (is_null(array_pop($chuncks))) return "/";
} else {
$chuncks[] = $part;
}
}
$output = implode("/", $chuncks);
if ($absolute) $output = "/".$output;
return $output;
}
/**
* Splits link (without .. or .) into parts along with direct url access.
* @param url
*
* Ex. /dir1/dir2/page.gmi
* --> "dir1" --> "/dir1"
* --> "dir2" --> "/dir1/dir2"
* --> "page.gmi" --> "/dir2/page.gmi"
*/
function split_path_links($path, $prefix="") {
$parts = array_filter(explode("/", $path), 'strlen');
if (empty($parts)) return array();
if ("/"==$path[0])
$stack = "/";
else
$stack = "";
$output = array();
$slash = "";
foreach ($parts as $part) {
$stack .= $slash.$part;
$output[$part] = $prefix.$stack;
$slash = "/";
}
return $output;
}
/** /**
* Parses the gemtext and generates the internal format version * Parses the gemtext and generates the internal format version
* @param str $fileContents the gemtext to parse * @param str $fileContents the gemtext to parse
@ -12,7 +61,7 @@ mb_regex_encoding("UTF-8");
function gemtextParser($fileContents) { function gemtextParser($fileContents) {
if (empty($fileContents)) return array(); if (empty($fileContents)) return array();
$fileContents = rtrim($fileContents); // removes last empty line $fileContents = rtrim($fileContents); // removes last empty line
$fileLines = mb_split("\R", $fileContents); // Unix, Mac, Windows line feeds $fileLines = mb_split("\n|\r\n?", $fileContents); // Unix, Mac, Windows line feeds
$mode = null; $mode = null;
$current = array(); $current = array();
foreach ($fileLines as $line) { foreach ($fileLines as $line) {
@ -182,8 +231,15 @@ class GemtextTranslate_html {
protected $pageTitle = ""; protected $pageTitle = "";
public $translatedGemtext; public $translatedGemtext;
function __construct($parsedGemtext, $textDecoration=true, $baseUrl=Null) { /**
$this->baseUrl = $baseUrl; * @param $parsedGemtext the gemtext internal format
* @param $textDecoration bool to interpret or not the text decoration
* @param $urlPrefix the prefix to prepend if the URL rewriting is not on
* @param $currentPageDir the current directory, to be used without URL rewriting
*/
function __construct($parsedGemtext, $textDecoration=true, $urlPrefix=null, $currentPageDir=null) {
$this->urlPrefix = $urlPrefix;
$this->currentPageDir = $currentPageDir;
if (empty($parsedGemtext)) $parsedGemtext = ""; if (empty($parsedGemtext)) $parsedGemtext = "";
// to delete the last empty lines // to delete the last empty lines
$parsedGemtext = rtrim($parsedGemtext); $parsedGemtext = rtrim($parsedGemtext);
@ -266,29 +322,6 @@ class GemtextTranslate_html {
$text = preg_replace("/ +/", " ", $text); $text = preg_replace("/ +/", " ", $text);
} }
/**
* Resolve $path interpretating / . and ..
* @param $path str
* @returns "/" if .. goes above the limit
*/
public static function resolve_path($path) {
if (empty($path)) return "";
$absolute = "/"==$path[0];
$parts = array_filter(explode("/", $path), 'strlen');
$chuncks = array();
foreach ($parts as $part) {
if ('.' == $part) continue;
if ('..' == $part) {
if (is_null(array_pop($chuncks))) return "/";
} else {
$chuncks[] = $part;
}
}
$output = implode("/", $chuncks);
if ($absolute) $output = "/".$output;
return $output;
}
public function translate($textDecoration=true) { public function translate($textDecoration=true) {
$output = ""; $output = "";
foreach ($this->parsedGemtext as $node) { foreach ($this->parsedGemtext as $node) {
@ -344,10 +377,10 @@ class GemtextTranslate_html {
preg_match("/^([^:]+):/", $link, $matches); preg_match("/^([^:]+):/", $link, $matches);
$protocol = @$matches[1]??"local"; $protocol = @$matches[1]??"local";
if ("local"==$protocol) { if ("local"==$protocol) {
if (!is_null($this->baseUrl)) { // No URL rewriting if (!is_null($this->urlPrefix)) { // No URL rewriting
if ($link[0]!="/") $link = "{$this->baseUrl}/$link"; $link = $this->currentPageDir."/".$link;
$link = self::resolve_path($link); $link = resolve_path($link);
$link = "/htmgem/index.php?url=$link"; $link = $this->urlPrefix.$link;
} }
$newWindow = ""; $newWindow = "";
} else { } else {

View File

@ -5,12 +5,10 @@ namespace htmgem\html;
mb_internal_encoding("UTF-8"); mb_internal_encoding("UTF-8");
mb_regex_encoding("UTF-8"); mb_regex_encoding("UTF-8");
/** $txt_icon = "H͜͡m ";
* Returns a full HTML page base
*/ function getHeader(\htmgem\GemtextTranslate_html $gt_html) {
function getFullHtml(\htmgem\GemtextTranslate_html $gt_html) {
$css = $gt_html->getCss(); $css = $gt_html->getCss();
if (!$css) $css = array("/htmgem/css/htmgem.css");
$output = <<<EOL $output = <<<EOL
<!DOCTYPE html> <!DOCTYPE html>
<html lang=""> <html lang="">
@ -23,21 +21,76 @@ EOL;
} }
$output .= <<<EOL $output .= <<<EOL
</head> </head>
<body>\n
EOL;
$output .= $gt_html->translatedGemtext;
$output .= "</body>\n</html>\n";
EOL;
return $output; return $output;
} }
function get404Gmipage($message, $url) { function array_key_last_slice($array) {
// array_key_last() only available as of php v7.3.0
return key(array_slice($array, -1));
}
/**
* @param $url the full URL to display
* @param $pageLink if not null, means no URL rewritting
*/
function getMenu(string $scheme, string $domain, string $path, string $prefix=null) {
global $txt_icon;
$links = \htmgem\split_path_links($path, $prefix);
// Removes the last part, as it won't hold a link
$lastLink = array_key_last_slice($links);
if ("index.gmi"==$lastLink) {
// removes the index page
array_pop($links);
$lastLink = array_key_last_slice($links);
}
array_pop($links);
$links = array($domain => "$prefix/") + $links;
$linkList = array();
foreach ($links as $label=>$link) {
$linkList []= "<a href='$link'>$label</a>\n";
}
$linkList [] = $lastLink."\n"; // The last part holds no link
$output = "<div class='menu-line'>\n";
$output .= "<strong>$txt_icon</strong>$scheme\n";
$output .= implode(" / ", $linkList);
$output .= "</div>\n";
return $output;
}
function getFooter(\htmgem\GemtextTranslate_html $gt_html) {
return "</body>\n</html>\n";
}
function getHtmlWithMenu($gt_html, $scheme, $domain, $path, $prefix=null) {
$menu = getMenu($scheme, $domain, $path, $prefix);
echo getHeader($gt_html);
echo "<body>\n";
echo "<div class='menu'>\n";
echo $menu;
echo "<hr>\n";
echo "</div>\n";
echo "<div id='gmi'>\n";
echo $gt_html->translatedGemtext;
echo "</div>\n";
echo "<div class='menu'>\n";
echo "<hr>\n";
echo $menu;
echo "</div>\n";
echo getFooter($gt_html);
}
function get404Gmipage($url) {
return <<<EOF return <<<EOF
# ⚠ $message # ⚠ $url ⚠
* **Page non trouvée**
* **Page not found**
**$url**
=> .. 🔄 🔄
EOF; EOF;
} }

View File

@ -5,89 +5,133 @@ require_once dirname(__FILE__)."/../lib-htmgem.inc.php";
final class miscTest extends TestCase { final class miscTest extends TestCase {
public function test_resolveLink(): void { public function test_split_path_links(): void {
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path(""), array(),
\htmgem\split_path_links(""),
"empty link"
);
$this->assertSame(
array(
"noslash" => "noslash",
),
\htmgem\split_path_links("noslash"),
"no slash"
);
$this->assertSame(
array(),
\htmgem\split_path_links("/"),
"only a slash"
);
$this->assertSame(
array(
"one" => "/one",
),
\htmgem\split_path_links("/one"),
"/one"
);
$this->assertSame(
array(
"one" => "one",
"two" => "one/two",
),
\htmgem\split_path_links("one/two"),
"one/two"
);
$this->assertSame(
array(
"one" => "/one",
"two" => "/one/two",
"file.ext" => "/one/two/file.ext",
),
\htmgem\split_path_links("/one/two/file.ext"),
"/one/two/file.ext"
);
}
public function test_resolve_path(): void {
$this->assertSame(
\htmgem\resolve_path(""),
"", "",
"empty link" "empty link"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("test"), \htmgem\resolve_path("test"),
"test", "test",
"single word" "single word"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path(" "), \htmgem\resolve_path(" "),
" ", " ",
"single space" "single space"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path(" A B "), \htmgem\resolve_path(" A B "),
" A B ", " A B ",
"several space" "several space"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("/"), \htmgem\resolve_path("/"),
"/", "/",
"one slash" "one slash"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("//"), \htmgem\resolve_path("//"),
"/", "/",
"two slashes" "two slashes"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("/////"), \htmgem\resolve_path("/////"),
"/", "/",
"five slashes" "five slashes"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/"), \htmgem\resolve_path("one/"),
"one", "one",
"strip the last slash" "strip the last slash"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("/two"), \htmgem\resolve_path("/two"),
"/two", "/two",
"slash at the beginning" "slash at the beginning"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("/two/"), \htmgem\resolve_path("/two/"),
"/two", "/two",
"slash at the beginning and the end" "slash at the beginning and the end"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/two/"), \htmgem\resolve_path("one/two/"),
"one/two", "one/two",
"only the last slash remains" "only the last slash remains"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/two/three//"), \htmgem\resolve_path("one/two/three//"),
"one/two/three", "one/two/three",
"strip the last slashes" "strip the last slashes"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/../"), \htmgem\resolve_path("one/../"),
"", "",
"empty one" "empty one"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/two/../"), \htmgem\resolve_path("one/two/../"),
"one", "one",
"empty one two" "empty one two"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/two/../.."), \htmgem\resolve_path("one/two/../.."),
"", "",
"empty one two twice" "empty one two twice"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/../two/./../three"), \htmgem\resolve_path("one/../two/./../three"),
"three", "three",
"waltz" "waltz"
); );
$this->assertSame( $this->assertSame(
\htmgem\GemtextTranslate_html::resolve_path("one/../.."), \htmgem\resolve_path("one/../.."),
"/", "/",
"directory traversal" "directory traversal"
); );