From 49307f74c0d4f1f32047ae5cbeb5bc933cdc6be2 Mon Sep 17 00:00:00 2001 From: sorenpeter Date: Fri, 22 Sep 2023 10:41:49 +0200 Subject: [PATCH] Second commit --- .gitignore | 4 +- add_url.php | 82 +++ follow.php | 88 +++ index.php | 59 +- libs/Base32.php | 81 +++ libs/gallery.php | 40 ++ libs/hash.php | 52 ++ {partials => libs}/session.php | 5 +- libs/twtxt.php | 415 ++++++++++++++ load_twt_files.php | 61 ++ views/login.php => login.php | 25 +- new_twt.php | 101 ++++ partials/base.php | 137 +++++ partials/footer.php | 3 + partials/header.php | 21 + partials/listSelect.php | 41 ++ partials/nav-bar.php | 0 partials/profile.php | 41 ++ partials/timeline.php | 37 ++ php_server.sh | 0 private/config.ini | 43 ++ session.php | 101 ++++ style.css | 261 +++++++++ style_pixelblog.css | 559 +++++++++++++++++++ wip_todo/router.php | 31 + wip_todo/views/following.php | 32 ++ wip_todo/views/gallery.php | 28 + wip_todo/views/home.php | 53 ++ views/home.php => wip_todo/views/profile.php | 0 29 files changed, 2371 insertions(+), 30 deletions(-) create mode 100644 add_url.php create mode 100644 follow.php create mode 100644 libs/Base32.php create mode 100644 libs/gallery.php create mode 100644 libs/hash.php rename {partials => libs}/session.php (72%) create mode 100644 libs/twtxt.php create mode 100644 load_twt_files.php rename views/login.php => login.php (59%) create mode 100644 new_twt.php create mode 100644 partials/base.php create mode 100644 partials/footer.php create mode 100644 partials/header.php create mode 100644 partials/listSelect.php create mode 100644 partials/nav-bar.php create mode 100644 partials/profile.php create mode 100644 partials/timeline.php mode change 100644 => 100755 php_server.sh create mode 100644 private/config.ini create mode 100644 session.php create mode 100644 style.css create mode 100644 style_pixelblog.css create mode 100644 wip_todo/router.php create mode 100644 wip_todo/views/following.php create mode 100644 wip_todo/views/gallery.php create mode 100644 wip_todo/views/home.php rename views/home.php => wip_todo/views/profile.php (100%) diff --git a/.gitignore b/.gitignore index 5cc9054..abe4fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ ehthumbs.db Icon? Thumbs.db -*.DS_Store \ No newline at end of file +*.DS_Store +*.txt +private/cache/ \ No newline at end of file diff --git a/add_url.php b/add_url.php new file mode 100644 index 0000000..34494d1 --- /dev/null +++ b/add_url.php @@ -0,0 +1,82 @@ + + + + + + twtxt + + + + +

twtxt

+
+
+ +
URL is invalid, check it!

+ + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/follow.php b/follow.php new file mode 100644 index 0000000..179b197 --- /dev/null +++ b/follow.php @@ -0,0 +1,88 @@ + + + + + + twtxt + + + + +

twtxt

+
+
+ +
+ +
+ +
+ +
+ +
+
+ + + diff --git a/index.php b/index.php index 98144f3..f639d30 100644 --- a/index.php +++ b/index.php @@ -1,23 +1,46 @@ - - case '/login': - require __DIR__ . $viewDir . 'login.php'; - break; + + + + + + Timeline + + - case '/timeline': - require __DIR__ . $viewDir . 'timeline.php'; - break; + - default: - http_response_code(404); - require __DIR__ . $viewDir . '404.php'; -} + +mainURL] = $parsedTwtxtFile; + include 'partials/profile.php'; + } +} ?> + +
+ + + +
+ + + + + diff --git a/libs/Base32.php b/libs/Base32.php new file mode 100644 index 0000000..6b34e8d --- /dev/null +++ b/libs/Base32.php @@ -0,0 +1,81 @@ + 4) + { + $remainderSize -= 5; + $c = $remainder & (self::BITS_5_RIGHT << $remainderSize); + $c >>= $remainderSize; + $res .= static::CHARS[$c]; + } + } + if ($remainderSize > 0) + { + // remainderSize < 5: + $remainder <<= (5 - $remainderSize); + $c = $remainder & self::BITS_5_RIGHT; + $res .= static::CHARS[$c]; + } + if ($padRight) + { + $padSize = (8 - ceil(($dataSize % 5) * 8 / 5)) % 8; + $res .= str_repeat('=', $padSize); + } + + return $res; + } + + public static function decode($data) + { + $data = rtrim($data, "=\x20\t\n\r\0\x0B"); + $dataSize = strlen($data); + $buf = 0; + $bufSize = 0; + $res = ''; + $charMap = array_flip(str_split(static::CHARS)); // char=>value map + $charMap += array_flip(str_split(strtoupper(static::CHARS))); // add upper-case alternatives + + for ($i = 0; $i < $dataSize; $i++) + { + $c = $data[$i]; + if (!isset($charMap[$c])) + { + if ($c == " " || $c == "\r" || $c == "\n" || $c == "\t") + continue; // ignore these safe characters + throw new Exception('Encoded string contains unexpected char #'.ord($c)." at offset $i (using improper alphabet?)"); + } + $b = $charMap[$c]; + $buf = ($buf << 5) | $b; + $bufSize += 5; + if ($bufSize > 7) + { + $bufSize -= 8; + $b = ($buf & (0xff << $bufSize)) >> $bufSize; + $res .= chr($b); + } + } + + return $res; + } +} + +class Base32hex extends Base32 +{ + const CHARS = '0123456789abcdefghijklmnopqrstuv'; // lower-case +} \ No newline at end of file diff --git a/libs/gallery.php b/libs/gallery.php new file mode 100644 index 0000000..e4f2423 --- /dev/null +++ b/libs/gallery.php @@ -0,0 +1,40 @@ +'; // FOR DEBUGING +//echo '
'; print_r($img_posts);    echo '
'; // FOR DEBUGING + +// Loop through each post and extract date and entry text: +foreach($img_posts as $post){ + + $date = preg_filter('/^(?[^\t]+)\t(?.+)/', '\1', $post) ; + $entry = preg_filter('/^(?[^\t]+)\t(?.+)/', '\2', $post) ; + $text_only = preg_filter('/!\[(.*?)\]\((.*?)\)/', '\1', $entry); // this gives the post without the markdown img links (not sure why, but it works) + $text_only = trim($text_only); + $text_only = strip_tags($text_only); + + preg_match_all('/!\[(?.*?)\]\((?.*?)\)/', $entry, $img_array); + //echo '
'; print_r($img_array);    echo '
'; // FOR DEBUGING + + foreach($img_array[url] as $img => $val) { + $url = $img_array[url][$img]; + //$alt = $img_array[alt][$img]; + //echo ''.$text_only.''; + echo ''.$text_only.''; + + } +} + +?> diff --git a/libs/hash.php b/libs/hash.php new file mode 100644 index 0000000..7761d9a --- /dev/null +++ b/libs/hash.php @@ -0,0 +1,52 @@ += 2) { + $dateStr = $explodedLine[0]; + $twtContent = $explodedLine[1]; + + // dateStrings without timezone should be assumed as UTC + $dt = new DateTime($dateStr); + + // Getting the new formatted datetime + //$dateStr = $dt->format(DateTime::ATOM); // Updated ISO8601 + $dateStr = $dt->format(DateTime::RFC3339); + $dateStr = str_replace('+00:00', 'Z', $dateStr); + $dateStr = str_replace('-00:00', 'Z', $dateStr); + + $hashPayload = "$url\n$dateStr\n$twtContent"; + + // Default to 32 bytes + // https://www.php.net/manual/en/function.sodium-crypto-generichash.php + $hashBytes = sodium_crypto_generichash($hashPayload); + $hashStr = substr(Base32::encode($hashBytes), -7); + + return $hashStr; + } + + return 'INVALID'; +} + +function checkValidHashes() { + $url = 'http://magical.fish:70/feeds/twtxt/twtxt.txt'; + + $twt = "2023-06-17T00:33:32-06:00\tSun's out funs out!"; + $expectedHash = 'ujcbz3q'; + + assert(getHashFromTwt($twt, $url) === $expectedHash); + + $twt = "2023-06-20T07:51:48-06:00\tWhat a way to go."; + $expectedHash = 'f7hzthq'; + assert(getHashFromTwt($twt, $url) === $expectedHash); + + echo 'Asserts passed'; +} \ No newline at end of file diff --git a/partials/session.php b/libs/session.php similarity index 72% rename from partials/session.php rename to libs/session.php index 52a1155..5407aab 100644 --- a/partials/session.php +++ b/libs/session.php @@ -1,10 +1,13 @@ [ + 'peer_name' => 'generic-server', + 'verify_peer' => FALSE, + 'verify_peer_name' => FALSE, + 'allow_self_signed' => TRUE + ] + ] +); +curl_setopt($curl, CURLOPT_SSLVERSION, 4); +*/ + +/** + * The function searches for a key-value pair in a string and returns the value if found. + * + * @param keyToFind The key we want to find in the string. + * @param string The string in which to search for the key-value pair. + * + * @return the value of the key that matches the given keyToFind in the given string. If a match is + * found, the function returns the value of the key as a string after trimming any whitespace. If no + * match is found, the function returns null. + */ +function getSingleParameter($keyToFind, $string) { + if (!str_contains($string, $keyToFind)) { + return null; + } + + $pattern = '/\s*' . $keyToFind . '\s*=\s*([^#\n]+)/'; + //$pattern = '/\s*' . $keyToFind . '\s*=\s*([^\s#]+)/'; // Only matches the first word + preg_match($pattern, $string, $matches); + + if (isset($matches[1])) { + return trim($matches[1]); + } + + return null; +} + +function getDoubleParameter($keywordToFind, $string) { + // Returns string or null + $pattern = '/#\s*' . preg_quote($keywordToFind, '/') . '\s*=\s*(\S+)\s*(\S+)/'; + // Matches "# = " + preg_match($pattern, $string, $matches); + + if (isset($matches[1]) && isset($matches[2])) { + $result = array($matches[1], $matches[2]); + return $result; + } + + return null; +} + +function getReplyHashFromTwt(string $twtString): string { + // Extract the text between parentheses using regular expressions + $pattern = '/\(#([^\)]+)\)/'; // Matches "(#)" + preg_match($pattern, $twtString, $matches); + + if (isset($matches[1])) { + $textBetweenParentheses = $matches[1]; + return $textBetweenParentheses; + } + + return ''; +} + +function getMentionsFromTwt(string $twtString) { + $pattern = '/@<([^>]+)\s([^>]+)>/'; // Matches "@" + preg_match_all($pattern, $twtString, $matches, PREG_SET_ORDER); + + $result = array(); + + foreach ($matches as $match) { + $nick = $match[1]; + $url = $match[2]; + $result[] = array("nick" => $nick, "url" => $url); + } + + return $result; +} + +function replaceMentionsFromTwt(string $twtString): string { + // Example input: 'Hello @, how are you? @'; + // Example output: Hello @eapl.mx@eapl.mx/twtxt.txt, how are you? @nick@server.com/something/twtxt.txt + + #$pattern = '/@<([^ ]+)\s([^>]+)>/'; + #$replacement = '@$1'; + #$twtString = '@'; + $pattern = '/@<([^ ]+) ([^>]+)>/'; + $replacement = '@$1'; + + $result = preg_replace($pattern, $replacement, $twtString); + + return $result; +} + +function replaceLinksFromTwt(string $twtString) { + // Regular expression pattern to match URLs + $pattern = '/(?$1'; + $result = preg_replace($pattern, $replacement, $twtString); + + return $result; +} + +function replaceMarkdownLinksFromTwt(string $twtString) { + $pattern = '/\[([^\]]+)\]\(([^)]+)\)/'; + + $replacement = '$1'; + $result = preg_replace($pattern, $replacement, $twtString); + + return $result; +} + +function replaceImagesFromTwt(string $twtString) { + $pattern = '/!\[(.*?)\]\((.*?)\)/'; + $replacement = '$1'; + $result = preg_replace($pattern, $replacement, $twtString); + + return $result; +} + +function getTimeElapsedString($timestamp, $full = false) { + $now = new DateTime; + $ago = new DateTime; + $ago->setTimestamp($timestamp); + + $agoText = 'ago'; + if ($now < $ago) { + $agoText = 'in the future'; + } + + $diff = $now->diff($ago); + + //$diff->w = floor($diff->d / 7); + $w = floor($diff->d / 7); + $d = $diff->d - ($w * 7); + //$diff->d -= $diff->w * 7; + + $string = array( + 'y' => 'year', + 'm' => 'month', + 'w' => 'week', + 'd' => 'day', + 'h' => 'hour', + 'i' => 'minute', + 's' => 'second', + ); + foreach ($string as $k => &$v) { // k is key, and v is value... Obviously + if ($k === 'w') { + if ($w) { + $v = $w . ' ' . $v . ($w > 1 ? 's' : ''); + } else { + unset($string[$k]); + } + } else { + if ($diff->$k) { + $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : ''); + } else { + unset($string[$k]); + } + } + } + + if (!$full) $string = array_slice($string, 0, 1); + return $string ? implode(', ', $string) . " $agoText" : 'just now'; +} + +function getCachedFileContentsOrUpdate($fileURL, $cacheDurationSecs = 15) { + # TODO: Process the Warning + # Warning: file_get_contents(https://eapl.mx/twtxt.net): + # failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in + + $cacheFilePath = getCachedFileName($fileURL); + + // Check if cache file exists and it's not expired + if (file_exists($cacheFilePath) && (time() - filemtime($cacheFilePath)) < $cacheDurationSecs) { + return file_get_contents($cacheFilePath); + } + + // File doesn't exist in cache or has expired, so fetch and cache it + $contents = file_get_contents($fileURL); + file_put_contents($cacheFilePath, $contents); + + return $contents; +} + +function getCachedFileContents($filePath) { + $cacheFile = getCachedFileName($filePath); + + // Check if cache file exists and it's not expired + if (file_exists($cacheFile)) { + return file_get_contents($cacheFile); + } + + return null; +} + +function updateCachedFile($filePath, $cacheDurationSecs = 15) { + $cacheFilePath = getCachedFileName($filePath); + + // File doesn't exist in cache or has expired, so fetch and cache it + // TODO: Seems it's not working right! + $fileDoesntExist = !file_exists($cacheFilePath); + $fileIsOld = false; + if (!$fileDoesntExist) { + $fileIsOld = !((time() - filemtime($cacheFilePath)) < $cacheDurationSecs); + } + + if ($fileDoesntExist || $fileIsOld) { + #echo "Loading Cached file $cacheFilePath
\n"; + $contents = @file_get_contents($filePath); + + if ($contents === false) { + // File loaded with errors, skip saving it + return; + } + + file_put_contents($cacheFilePath, $contents); + } +} + +function getTwtsFromTwtxtString($url) { + $fileContent = getCachedFileContents($url); + + if (is_null($fileContent)) { + return null; + } + $fileContent = mb_convert_encoding($fileContent, 'UTF-8'); + + $fileLines = explode("\n", $fileContent); + + $twtxtData = new TwtxtFile(); + + foreach ($fileLines as $currentLine) { + // Remove empty lines + if (empty($currentLine)) { + continue; + } + + if (str_starts_with($currentLine, '#')) { + // Check if comments (starting with #) have some metadata + if (!is_null(getSingleParameter('url', $currentLine))) { + $currentURL = getSingleParameter('url', $currentLine); + + if (empty($twtxtData->URLs)) { + $twtxtData->mainURL = $currentURL; + } + $twtxtData->URLs[] = $currentURL; + } + if (!is_null(getSingleParameter('nick', $currentLine))) { + $twtxtData->nick = getSingleParameter('nick', $currentLine); + } + if (!is_null(getSingleParameter('avatar', $currentLine))) { + $twtxtData->avatar = getSingleParameter('avatar', $currentLine); + } + if (!is_null(getSingleParameter('emoji', $currentLine))) { + $twtxtData->emoji = getSingleParameter('emoji', $currentLine); + } + if (!is_null(getSingleParameter('lang', $currentLine))) { + $twtxtData->lang = getSingleParameter('lang', $currentLine); + } + if (!is_null(getSingleParameter('description', $currentLine))) { + $twtxtData->description = getSingleParameter('description', $currentLine); + // TODO - FIX BUG: only takes first word! + } + if (!is_null(getSingleParameter('follow', $currentLine))) { + $twtxtData->following[] = getSingleParameter('follow', $currentLine); + } + } + + if (!str_starts_with($currentLine, '#')) { + $explodedLine = explode("\t", $currentLine); + if (count($explodedLine) >= 2) { + $dateStr = $explodedLine[0]; + $twtContent = $explodedLine[1]; + + $twtContent = replaceMentionsFromTwt($twtContent); + + // Convert HTML problematic characters + $twtContent = htmlentities($twtContent); + + // Replace the Line separator character (U+2028) + // \u2028 is \xE2 \x80 \xA8 in UTF-8 + // Check here: https://www.mclean.net.nz/ucf/ + //$twtContent = str_replace("\xE2\x80\xA8", "
\n", $twtContent); + + // For some reason I was having trouble finding this nomenclature + // that's why I leave the UTF-8 representation for future reference + $twtContent = str_replace("\u{2028}", "\n
\n", $twtContent); + $twtContent = replaceLinksFromTwt($twtContent); + $twtContent = replaceImagesFromTwt($twtContent); + $twtContent = replaceMarkdownLinksFromTwt($twtContent); + + // Get and remote the hash + $hash = getReplyHashFromTwt($twtContent); + if ($hash) { + $twtContent = str_replace("(#$hash)", '', $twtContent); + } + + // TODO: Get mentions + $mentions = getMentionsFromTwt($twtContent); + + + // Get Lang metadata + + if (($timestamp = strtotime($dateStr)) === false) { + //echo "The string ($dateStr) is incorrect"; + // Incorrect date string, skip this twt + continue; + } else { + $displayDate = getTimeElapsedString($timestamp); + } + + // TODO: Only 1 twt by second is allowed here + $twt = new Twt(); + + $twt->originalTwtStr = $currentLine; + $twt->hash = getHashFromTwt($currentLine, $twtxtData->mainURL); + $twt->fullDate = date('j F Y h:i:s A', $timestamp) . ' (UTC)'; + $twt->displayDate = $displayDate; + $twt->content = $twtContent; + $twt->replyToHash = $hash; + $twt->mentions = $mentions; + $twt->avatar = $twtxtData->avatar; + $twt->emoji = $twtxtData->emoji; + $twt->nick = $twtxtData->nick; + $twt->mainURL = $twtxtData->mainURL; + + $twtxtData->twts[$timestamp] = $twt; + // TODO: Interpret the content as markdown + } + } + } + + return $twtxtData; +} + +function insertFollowingURL($urlString) { + // Check if it's a valid URL + // Retrieve the nickname, if didn't find a nick, ask for one + + $originalCode = ' + Lorem ipsum dolor sit amet, + #~~~# + consectetur adipiscing elit.'; + + $text = '#~~~#'; + $newText = '123' . PHP_EOL . $text; + $result = str_replace('#~~~#', $newText, $originalCode); + + echo $result; +} + +function getCachedFileName($filePath) { + return __DIR__ . '/../private/cache/' . hash('sha256', $filePath); // TODO: make better path +} + +if (!function_exists('str_starts_with')) { + function str_starts_with($haystack, $needle) { + return (string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; + } +} +if (!function_exists('str_ends_with')) { + function str_ends_with($haystack, $needle) { + return $needle !== '' && substr($haystack, -strlen($needle)) === (string)$needle; + } +} +if (!function_exists('str_contains')) { + function str_contains($haystack, $needle) { + return $needle !== '' && mb_strpos($haystack, $needle) !== false; + } +} diff --git a/load_twt_files.php b/load_twt_files.php new file mode 100644 index 0000000..f93d233 --- /dev/null +++ b/load_twt_files.php @@ -0,0 +1,61 @@ +\n
\n"; +#ob_flush(); + +const DEBUG_TIME_SECS = 300; +const PRODUCTION_TIME_SECS = 5; +$fileContent = getCachedFileContentsOrUpdate($url, PRODUCTION_TIME_SECS); +$fileContent = mb_convert_encoding($fileContent, 'UTF-8'); + +$fileLines = explode("\n", $fileContent); + +$twtFollowingList = []; +foreach ($fileLines as $currentLine) { + if (str_starts_with($currentLine, '#')) { + if (!is_null(getDoubleParameter('follow', $currentLine))) { + $twtFollowingList[] = getDoubleParameter('follow', $currentLine); + } + } +} + +# Load all the files +# Save a flag to know it's loading files in the background +foreach ($twtFollowingList as $following) { + #echo "Updating: $following[1]
\n"; + #ob_flush(); + updateCachedFile($following[1]); +} +#echo 'Finished'; +#ob_flush(); + +header('Location: .'); +exit(); diff --git a/views/login.php b/login.php similarity index 59% rename from views/login.php rename to login.php index bb2ddb0..21d5782 100644 --- a/views/login.php +++ b/login.php @@ -1,5 +1,5 @@ @@ -12,21 +12,26 @@ + //$config = parse_ini_file('private/config.ini'); + //$password = $config['password']; + + if( isset($_SESSION['password'])) { + if($_SESSION['password']=="$password") + { + header("Location: /"); + die(); + ?>

You are loggged in now

-
- + + +

Log in:

-
+
-

+

diff --git a/new_twt.php b/new_twt.php new file mode 100644 index 0000000..114da57 --- /dev/null +++ b/new_twt.php @@ -0,0 +1,101 @@ + + + + + + twtxt + + + + +

twtxt

+
+
+ +
+ +
+
+ + + diff --git a/partials/base.php b/partials/base.php new file mode 100644 index 0000000..383f441 --- /dev/null +++ b/partials/base.php @@ -0,0 +1,137 @@ +mainURL] = $parsedTwtxtFile; + } + +} else { // Show timeline for the URL + $parsedTwtxtFiles = []; + foreach ($fileLines as $currentLine) { + if (str_starts_with($currentLine, '#')) { + if (!is_null(getDoubleParameter('follow', $currentLine))) { + $follow = getDoubleParameter('follow', $currentLine); + $twtFollowingList[] = $follow; + + // Read the parsed files if in Cache + $followURL = $follow[1]; + $parsedTwtxtFile = getTwtsFromTwtxtString($followURL); + if (!is_null($parsedTwtxtFile)) { + $parsedTwtxtFiles[$parsedTwtxtFile->mainURL] = $parsedTwtxtFile; + } + } + } + } +} + +$twts = []; + +# Combine all the followers twts +foreach ($parsedTwtxtFiles as $currentTwtFile) { + if (!is_null($currentTwtFile)) { + $twts += $currentTwtFile->twts; + } +} + +if (!empty($_GET['hash'])) { + $hash = $_GET['hash']; + $twts = array_filter($twts, function($twt) use ($hash) { + return $twt->hash === $hash || $twt->replyToHash === $hash; + }); +} + + +krsort($twts, SORT_NUMERIC); + +if (!empty($_GET['hash'])) { + $twts = array_reverse($twts, true); +} + +$page = 1; +if (!empty($_GET['page'])) { + $page = intval($_GET['page']); +} + +$startingTwt = (($page - 1) * TWTS_PER_PAGE); +$twts = array_slice($twts, $startingTwt, TWTS_PER_PAGE); diff --git a/partials/footer.php b/partials/footer.php new file mode 100644 index 0000000..4bcf3b0 --- /dev/null +++ b/partials/footer.php @@ -0,0 +1,3 @@ +
+ © 2023 Søren Peter Mørch +
\ No newline at end of file diff --git a/partials/header.php b/partials/header.php new file mode 100644 index 0000000..d55afcb --- /dev/null +++ b/partials/header.php @@ -0,0 +1,21 @@ +
+ +
\ No newline at end of file diff --git a/partials/listSelect.php b/partials/listSelect.php new file mode 100644 index 0000000..cdb0c07 --- /dev/null +++ b/partials/listSelect.php @@ -0,0 +1,41 @@ + +
+ + + +
\ No newline at end of file diff --git a/partials/nav-bar.php b/partials/nav-bar.php new file mode 100644 index 0000000..e69de29 diff --git a/partials/profile.php b/partials/profile.php new file mode 100644 index 0000000..7134f40 --- /dev/null +++ b/partials/profile.php @@ -0,0 +1,41 @@ + + +
+ + + + + +
+ + nick ?>@mainURL, PHP_URL_HOST); ?> + + +
description ?>
+ + + +
+ + + +
diff --git a/partials/timeline.php b/partials/timeline.php new file mode 100644 index 0000000..af4116e --- /dev/null +++ b/partials/timeline.php @@ -0,0 +1,37 @@ + + + + + + + \ No newline at end of file diff --git a/php_server.sh b/php_server.sh old mode 100644 new mode 100755 diff --git a/private/config.ini b/private/config.ini new file mode 100644 index 0000000..f5342bd --- /dev/null +++ b/private/config.ini @@ -0,0 +1,43 @@ +; timeline / twtxt-php +; Copy this file into a config.ini file and edit the following settings + +[main_settings] +; Enable to display PHP errors +; true or false +debug_mode = false + +; Time to wait before reloading URLs +cache_refresh_time = 15 + +; Max execution time to avoid running to the infinite +max_execution_time = 300 + +; Check that your current user has permissions for this file +; Check also the user owner is correct, www-data for instance +; TODO: Implement a way to know we have access to this file +; since there are many different conditions for not having +; access. +txt_file_path = "twtxt.txt" + +; Full URL for your public twtxt.txt file +public_txt_url = "https://darch.dk/twtxt-lists/twtxt.txt" +public_avatar = "https://darch.dk/twtxt-lists/avatar.png" +public_nick = "Timeline-DEV" + +; Check available timezones here: +; https://www.php.net/manual/en/timezones.php +timezone = "Europe/Copenhagen" + +twts_per_page = 50 + +[security] +; Generate it with the TOTP module +totp_digits = 10 +totp_secret = "LZM25BDJPRVTNFZQBDOPQKFSKUAAS6BI" + +; It's recommended that your site is hosted on HTTPS +; In case it's in HTTP (not secure), set this to false +secure_cookies = true + +; Simple password for unnamed user +password = "" \ No newline at end of file diff --git a/session.php b/session.php new file mode 100644 index 0000000..907fb00 --- /dev/null +++ b/session.php @@ -0,0 +1,101 @@ + 'twtxt_session', + 'use_strict_mode' => true, + 'cookie_httponly' => true, + 'cookie_secure' => $config['secure_cookies'], + 'sid_length' => 64, + 'sid_bits_per_character' => 6, + 'cookie_samesite' => 'Strict', // Not compatible with PHP lower than 7.3 +]); + +function has_valid_session() { + $config = parse_ini_file('private/config.ini'); + $secretKey = $config['password']; + + if (isset($_SESSION['valid_session'])) { + return true; + } + + $cookieVal = decodeCookie($secretKey); + if ($cookieVal === false) { + #echo "Invalid cookie"; + return false; + } + + return true; +} + +function encrypt(string $data, string $key, string $method): string { + $ivSize = openssl_cipher_iv_length($method); + $iv = openssl_random_pseudo_bytes($ivSize); + $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); + # PHP 8.2 - Deprecated: implode(): + # Passing null to parameter #1 ($separator) of type array|string is deprecated + //$encrypted = strtoupper(implode(null, unpack('H*', $encrypted))); + $encrypted = strtoupper(implode(unpack('H*', $encrypted))); + + return $encrypted; +} + +function decrypt(string $data, string $key, string $method): string { + $data = pack('H*', $data); + $ivSize = openssl_cipher_iv_length($method); + $iv = openssl_random_pseudo_bytes($ivSize); + $decrypted = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); + + return trim($decrypted); +} + +function saveLoginSuccess($secretKey) { + // Set a cookie to remember the user + $_SESSION['valid_session'] = true; + + // Set a cookie value to remember the user + $encoded_cookie_value = generateCookieValue('admin', $secretKey); + $cookie_expiry = time() + (30 * 24 * 60 * 60); // 30 days + + $config = parse_ini_file('private/config.ini'); + + setcookie(COOKIE_NAME, $encoded_cookie_value, [ + 'expires' => $cookie_expiry, + 'secure' => $config['secure_cookies'], + 'httponly' => true, + 'samesite' => 'Strict', + ]); +} + +function generateCookieValue($username, $secretKey) { + $key = bin2hex($secretKey); + + $encrypted = encrypt($username, $key, ENCRYPTION_METHOD); + return $encrypted; +} + +function decodeCookie($secretKey) { + // Retrieve the encoded cookie name + if (!isset($_COOKIE[COOKIE_NAME])) { + return false; + } + + $encoded_cookie_value = $_COOKIE[COOKIE_NAME]; + $key = bin2hex($secretKey); + + $config = parse_ini_file('private/config.ini'); + + // Extend expiry by 30 days + $cookie_expiry = time() + (30 * 24 * 60 * 60); + setcookie(COOKIE_NAME, $encoded_cookie_value, [ + 'expires' => $cookie_expiry, + 'secure' => $config['secure_cookies'], + 'httponly' => true, + 'samesite' => 'Strict', + ]); + + $decrypted = decrypt($encoded_cookie_value, $key, ENCRYPTION_METHOD); + return $decrypted; +} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..a2f38e7 --- /dev/null +++ b/style.css @@ -0,0 +1,261 @@ +* { +/* border: thin solid pink;*/ +} + +/* from PaperMod: https://github.com/adityatelange/hugo-PaperMod/tree/54a3c6073518005182f3c3250ddb7e8c0cacd7ad/assets/css */ +:root { + --gap: 1rem; + --content-gap: 20px; + --nav-width: 1024px; + --main-width: 720px; + --header-height: 60px; + --footer-height: 60px; + --radius: 8px; + --theme: rgb(255, 255, 255); + --entry: rgb(255, 255, 255); + --primary: rgb(30, 30, 30); + --secondary: rgb(108, 108, 108); + --tertiary: rgb(214, 214, 214); + --content: rgb(31, 31, 31); + --hljs-bg: rgb(28, 29, 33); + --code-bg: rgb(245, 245, 245); + --border: rgb(238, 238, 238); +} + +body { + background-color: var(--code-bg); + font-family: sans-serif; + max-width: 700px; + margin: 1rem auto; + line-height: 1.25; +} + +a, button, body, h1, h2, h3, h4, h5, h6 { + color: var(--primary); +} + +/* == Forms (from https://adi.tilde.institute/default.css/) ==================== */ + +form { + align-content: center; + display: flex; +} + +label { + display: block; + font-size: small; +} + +input, textarea, select { + box-sizing: border-box; + display: inline-block; +/* margin: .5ex 0 1ex 0;*/ +margin-top: -0.5rem; + padding: 1ex .5rem; + border: thin solid var(--border); + border-radius: var(--radius); + /*width: 100%;*/ + font-size: 1em; + background-color: var(--entry); + color: var(--primary); +} + +input[type="text"], +input[type="file"] { + flex-grow: 1; + margin-right: 0.5rem; +} + +input[type=checkbox], input[type=radio] { + display: inline; + width: auto; +} + +textarea { + font-family: inherit; + width: 100%; + background-color: var(--input-box); +} + +input[type="submit"], +a.button, button { + background-color: var(--pod-color); + border-radius: var(--radius); + border: thin solid var(--pod-color); + color: var(--button-text); + display: block; +/* margin-top: -1rem;*/ + font-size: 1em; + padding: 1ex 1rem; + text-align: center; + text-decoration: none; +} + +button.primary { + width: 100%; + margin: 1rem 0; +} + +fieldset { + border: thin solid var(--border-color); + border-radius: var(--radius); + margin: 0.5rem 0; + padding: 0.5rem 0.75rem; +} + +/* --- */ + +.right { + float: right; +} + + +hr { + background: var(--border-color); + border: 0; + height: 1px; + width: 100%; +} + + +nav { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + max-width: calc(var(--nav-width) + var(--gap) * 2); + margin-inline-start: auto; + margin-inline-end: auto; +} + +nav ul { + width: 100%; + display: flex; + justify-content: space-between; + padding-left: 0.5rem; +} + +nav ul li { + display: inline-block; + list-style: none; +} + +nav a { + text-decoration: none; + font-weight: bold; + line-height: 1.5; +} + +nav form { + margin: 0; +} + +nav .link-btn { + background: none; + border: none; + cursor: pointer; + margin: 0; + padding: 0; + color: var(--primary); + font-weight: bold; +/* text-decoration: underline;*/ +} + +img { + object-fit: contain; + max-width: 100%; + height: auto; + border-radius: 0.25rem; +} + +img.avatar { + height: 48px; + width: 48px; + object-fit: cover; +} + +a.author { + text-decoration: none; + color: var(--primary); +} + +.profile { + padding: 0 1rem; + display: grid; +/* grid-template-columns: 4rem 3fr 1fr;*/ + grid-template-columns: 4rem 1fr; + grid-gap: 1rem; + margin: 1rem 0; +} + +.profile img.avatar { + width: 4rem; + height: 4rem; +/* border-radius: 100%;*/ +} + +.profile .author { + font-size: larger; +} + + +.profile blockquote { + margin: 0.5rem 0; + color: var(--secondary); +} + +.profile nav a { + font-size: small; + text-decoration: underline; +} + +article { + background-color: var(--entry); + color: var(--secondary); + border-radius: 0.25rem; + border: thin solid var(--border); + margin: 0.5rem; + padding: 0.5rem; +} + +article.post-entry { + display: grid; + grid-gap: var(--gap); + grid-template-columns: 48px auto; +} + +article .twt-msg { + padding: 0.5rem 0; +} + +article .twt-msg img { + margin: 0.25rem -0.25rem; + border: thin solid var(--border); +} + +article .twt-msg > img:first-child { + margin-top: 0; +} + +article .twt-msg > img:last-child { + margin-bottom: 0; +} + +article small { + padding-left: 0.15rem ; +} + +article small .right{ + padding-right: 0.25rem; +} + +nav.pagnation { + display: flex; + justify-content: center; + padding: 0.5rem 0; +} + +footer { + border-top: thin solid var(--border); + margin-top: 1rem; + text-align: center; +} \ No newline at end of file diff --git a/style_pixelblog.css b/style_pixelblog.css new file mode 100644 index 0000000..bf964ec --- /dev/null +++ b/style_pixelblog.css @@ -0,0 +1,559 @@ +/* == Variables == */ + +:root { + --roundness: 0.25rem; + --pod-color: #222; + /*--pod-muted: #3623BD;*/ + /*--bg-body: #fff;*/ + /*--bg-post: #F7F7F7;*/ + /*--bg-code: #eee;*/ + /*--code-color: #D22F27;*/ + /*--text-color: #444;*/ + /*--text-small: #aaa;*/ + /*--icon-color: #888;*/ + --link-color: blue; + --link-visited: purple; + --link-active: red; + /*--link-nav: #222;*/ + /*--input-box: #fff;*/ + --input-border: #ccc; + --button-text: #fff; + --border-color: #ccc; + --warning: #941100; +} + +/* == Meta Classes ======================================== */ + + .left { + float: left; + /*text-align: left;*/ + } + .center { + float: center; + text-align: center; + } + .right { + float: right; + /*text-align: right;*/ + } + + .avoidwrap { + display:inline-block; + } + + .fullwidth { + display: block; + } + +/* Minimal grid system with auto-layout columns (from pico.css) */ + +/* .grid { + grid-column-gap: 1rem; + grid-row-gap: 1rem; + display: grid; + grid-template-columns: 1fr; + margin: 0; } + + @media (min-width: 992px) { + .grid { + grid-template-columns: repeat(auto-fit, minmax(0%, 1fr)); } } + .grid > * { + min-width: 0; }*/ + +/* == Typography ============================ */ + + body { + color: var(--text-color); + margin: 0 auto; + max-width: 85ch; + padding: 0.5rem 0; + background-color: var(--bg-body); + font-weight: 400; + padding: 0.5rem; + } + body, input { + font-family: system-ui, -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; /* To use the system default font. */ + line-height: 1.4; + } + + /* Links */ + + a { + color: var(--link-color); + /*text-decoration: none;*/ + } + a:visited { + color: var(--primary); + /*color: var(--link-visited);*/ + /*text-decoration: none;*/ + } + +/* == Icons ======================== */ + + .ti { + color: var(--icon-color); + padding-right: 0.25rem; + } + +/* == Header / Navigation ======================= */ + + .edit { + background-color: yellow; + display: flex; + justify-content: space-between; + } + + header { + display: block; + /*margin-top: 0.5rem;*/ + margin-bottom: 2rem; + } + + header a, + header a:visited { + color: var(--pod-color); + } + + header nav { + margin-bottom: 0.75rem; + } + + nav ul { + padding: 0; + margin: 0; + display: inline-block; + } + nav ul li { + display: inline-block; + margin: 0 0.2rem; + padding: 0 0.2rem; + } + + nav a, nav a:visited { + color: var(--link-nav); + /*text-decoration: none;*/ + } + +/* == Forms (from https://adi.tilde.institute/default.css/) ==================== */ + + form { + align-content: center; + display: flex; + } + + label { + display: block; + font-size: small; + } + + input, textarea, select { + box-sizing: border-box; + display: inline-block; +/* margin: .5ex 0 1ex 0;*/ +/* padding: 1ex .5em;*/ +/* padding: 0.25rem;*/ + border: thin solid var(--input-border); + border-radius: var(--roundness); + /*width: 100%;*/ + font-size: 1em; + background-color: var(--input-box); + color: var(--text-color); + } + + input[type="text"], + input[type="file"] { + flex-grow: 1; + margin-right: 0.5rem; + } + + input[type=checkbox], input[type=radio] { + display: inline; + width: auto; + } + + textarea { + font-family: inherit; + width: 100%; + background-color: var(--input-box); + } + + input[type="submit"], + a.button, button { + background-color: var(--pod-color); + border-radius: var(--roundness); + border: thin solid var(--pod-color); + color: var(--button-text); + display: block; +/* margin-top: -1rem;*/ + font-size: 1em; + padding: 1ex 1rem; + text-align: center; + text-decoration: none; + } + + button.primary { + width: 100%; + margin: 1rem 0; + } + + fieldset { + border: thin solid var(--border-color); + border-radius: var(--roundness); + margin: 0.5rem 0; + padding: 0.5rem 0.75rem; + } + + + hr { + background: var(--border-color); + border: 0; + height: 1px; + width: 100%; + } + + .avatar img { + width: 3rem; + height: 3rem; + object-fit: cover; + border-radius: var(--roundness); + } + +/* == Timeline styline ========================== */ + + /* Post with outline */ + article.h-entry { + margin: 1rem 0; + padding: 0.5rem; + background-color: var(--bg-post); + border-radius: var(--roundness); + /*border: solid thin var(--border-color);*/ + } + + .u-author { + display: flex; + flex-direction: row; + /*align-items: center;*/ + } + + article .p-name { + display: block; + left: 0px !important; + /*font-weight: 400;*/ + /*font-size: 34px;*/ + font-size: 1.2rem; + /*color: #1095c1;*/ + padding-bottom: 0.2rem; + } + + a.p-name, a.p-name:visited { + font-weight: 600; + color: var(--link-nav); + } + + article .p-org, + article .p-summary a em, + article .p-summary p a em { + font-style: normal; + left: 0px !important; + font-weight: 400; + color: var(--text-small); + white-space: nowrap; + } + + .p-summary { + /*padding: 0.5rem;*/ + } + + .author a { + text-decoration: none; + } + + + .author nav ul li { + margin-left: 0; + padding-left: 0; + } + +/* == Profile ===================== */ + +/* .profileCard { + margin: 1rem 0; + padding: 0.5rem; + background-color: var(--bg-post); + border-radius: var(--roundness); + border: solid thin var(--border-color); + } + + .profileCard .grid { + grid-template-columns: 3fr 1fr; + grid-gap: 1rem; + } + + .profileCard img { + height: 4.5rem; + width: 4.5rem; + margin-right: 0.5rem; + float: left; + } + + .profileCard div.bio { + margin-top: -0.75em; + margin-left: 1.7em; + word-break: break-all; + } + + .profileCard a.u-url.p-name { + display: block; + font-size: 1.7rem; + font-weight: 600; + margin: 0.75rem; + } + + .profileCard .p-org { + color: var(--text-small); + } + + .profileCard blockquote { + margin: 0.75rem; + } + + .profileCard aside { + text-align: center; + } + + .profileCard small.compact { + display: block; + } + + .profileCard summary { + text-align: center; + padding: 0; + } + + .profileCard details { + text-align: left; + } + + .profileCard a.button { + color: var(--button-text); + display: block; + text-align: center; + padding: 0.5rem; + font-weight: 600;*/ + /*color: var(--bg-post);*/ + /*background-color: var(--button-bg);*/ + /*border-radius: var(--roundness);*/ + /*border: solid thin #FFF; var(--bg-body);*/ + } + +/* .profileCard a.button.off { + background-color: var(--pod-color); + color: var(--button-text); + }*/ + +/* == Footer ===================== */ + +footer { + margin-top: 1rem; + border-top: 1px solid var(--pod-color); + padding: 0.25rem; + font-size: small; +} + +footer nav ul li { + margin: 0; + padding: 0 0.1rem; +} + +footer a, footer a:visited, +footer a .ti, footer a:visited .ti { + color: var(--pod-color); + padding: 0; +} + +/* Mobile Styling (from: vanillacss.com) */ +@media screen and (max-width: 85ch) { + + table { table-layout: auto; } + + .right { float: none; } + + /*.twt-hash { float: right; }*/ + + .profileCard .grid { + grid-template-columns: 1fr; + } + + header nav { + margin-bottom: 0; + } + + nav.pod-menu ul.right { + display: grid; + /*grid-template-columns: auto auto auto; */ + font-size: initial; + } + + nav.user-menu { + padding: 0; + } + + nav.user-menu ul { + display: grid; + } + + nav ul li { + margin-bottom: 0.5rem; + } + + nav.toolbar-nav ul.right { + display: grid; + } + + nav.toolbar-nav #post, + button { + width: 100%; + } + + footer nav ul { + margin: 0.25rem auto; + display: block; + text-align: center; + } +} + + +/* === Picoblog.css by darch.dk */ + +main li { + /*background-color: var(--bg-post); */ + margin-left: -2.5rem; + list-style: none; + /*border: solid thin var(--border-color);*/ + /*border-radius: var(--roundness);*/ + padding: 10px; + /*margin-bottom: 0.5rem;*/ + /*border-top: solid thin var(--border-color);*/ +} + +li a.date { + display: block; + font-size: small; + text-align: right; + color: var(--text-small); + margin: 0.5rem 0; + padding: 0.2rem 0.5rem; + border-bottom: solid thin var(--border-color); +} + +li a.date:hover { + /*color: var(--link-active);*/ + /*border-color: var(--link-active);*/ +} + + +/* http://jsbin.com/giqovotudi/edit?html,css,output */ + +p.grid { /*https://smolcss.dev/*/ + --min: 15ch; + --gap: 0.5rem; + display: flex; + flex-wrap: wrap-reverse; + gap: var(--gap); + margin-left: 0.5rem; +} +p.grid > a.image { + flex: 1 1 var(--min); + margin-top: 1rem; +} + + +img { + display: block; + /*margin-top: 0.5rem;*/ + max-width: 100%; + object-fit: cover; + border-radius: var(--roundness); +/* border: thin solid var(--border-color);*/ +} + + +/* == Timeline ===================== */ + +/*.timeline a img { + display: block; + cursor:zoom-in; + margin: 0.5rem auto; +}*/ + +p.grid a img { + margin: -0.5rem; +} + +@media screen and (max-width: 760px) { + body { + /* Center body in page */ + margin: 0.5rem; + } + + h1 { + font-size: -0.5rem; + } + + li { + margin-left: -2.5rem; + /*margin-right: 0.5rem;*/ + } +} + +.warning { + color: var(--warning); +} + +/* == Gallery ===================== */ + +.gallery { + max-width: 1200px; + margin: 0 auto ; + display: grid; + grid-gap: 0.75rem; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + /*grid-auto-rows: 225px;*/ + /*grid-auto-flow: dense;*/ +} + +.gallery a img { + /* https://web.dev/aspect-ratio/ */ + aspect-ratio: 1 / 1; + width: 100%; + /*height: 100%;*/ + /*object-fit: cover;*/ + /*background-color: var(--border-color);*/ +} + +/* == Posting / Upload ===================== */ + +.upload .ti, +.posting .ti { + color: var(--code-color); + font-size: large; +} + +.upload img { + width: 12ch; + height: initial; + /*margin: 0.25rem auto;*/ +} + +.upload code { + padding: 0.25rem; + color: var(--code-color); + background-color: var(--input-box); + font-size: smaller; +} + +.upload a.button { + width: fit-content; + block-size: fit-content; + margin: 0.5rem auto; +} \ No newline at end of file diff --git a/wip_todo/router.php b/wip_todo/router.php new file mode 100644 index 0000000..8bb254c --- /dev/null +++ b/wip_todo/router.php @@ -0,0 +1,31 @@ +Oops!"; + //require __DIR__ . $viewDir . '404.php'; +} diff --git a/wip_todo/views/following.php b/wip_todo/views/following.php new file mode 100644 index 0000000..0a1b66a --- /dev/null +++ b/wip_todo/views/following.php @@ -0,0 +1,32 @@ + + + + + + + + <?=$title?> - Timeline + + + + + + + Following: feeds + + + + + + + + + + +
NickURL
+ + + +
+ + \ No newline at end of file diff --git a/wip_todo/views/gallery.php b/wip_todo/views/gallery.php new file mode 100644 index 0000000..d48ab54 --- /dev/null +++ b/wip_todo/views/gallery.php @@ -0,0 +1,28 @@ + + + + + + + + + <?=$title?> - Gallery + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wip_todo/views/home.php b/wip_todo/views/home.php new file mode 100644 index 0000000..1aa6520 --- /dev/null +++ b/wip_todo/views/home.php @@ -0,0 +1,53 @@ + + + + + + + + <?=$title?> - Timeline + + + + + + + + + +
+ +

+ + Twts for + + Timeline for + +

+ + + +
+ + + + + diff --git a/views/home.php b/wip_todo/views/profile.php similarity index 100% rename from views/home.php rename to wip_todo/views/profile.php