Merge branch 'main' into 34-different-time-zone-is-being-used-in-new-twts-and-in-replies

This commit is contained in:
eapl.me 2025-01-03 09:49:21 -06:00 committed by GitHub
commit e607df37a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 424 additions and 363 deletions

View file

@ -39,13 +39,13 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## 🛠 Installation and setup ## 🛠 Installation and setup
0. You need to have a webhosting with **PHP 8** and perferable running Apache or similar for timeline to work. 0. You need to have a webhosting with **PHP 8** and perferable running Apache or similar for timeline to work.
> There are free options, but I would suggest that you pay for your hosting and also get a nice domain, so you have more ownership over your data and online idetenty. > There are free options, but I would suggest that you pay for your hosting and also get a nice domain, so you have more ownership over your data and online idetenty.
1. Download the code from https://github.com/sorenpeter/timeline as a zip 1. Download the code from https://github.com/sorenpeter/timeline as a zip
2. Upload the content of the zip to you webhosting using a FTP client 2. Upload the content of the zip to you webhosting using a FTP client
- The default would be to put eveything from within the timeline-main folder in the root so you will have: - The default would be to put eveything from within the timeline-main folder in the root so you will have:
``` ```
@ -66,7 +66,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### Webfinger endpoint setup ### Webfinger endpoint setup
6. For allowing others to look you on using webfinger, you need to move the `.well-known` folder from within the `_webfinger_endpoint` to the root of your domain, so it is accesable from www.example.net/.well-know/webfinger 6. For allowing others to look you on using webfinger, you need to move the `.well-known` folder from within the `_webfinger_endpoint` to the root of your domain, so it is accesable from www.example.net/.well-know/webfinger
7. You also need to edit the `index.php` file wihtin the `.well-know/webfinger` folder and set the correct path for you timeline installation in `$timeline_dir` variable. 7. You also need to edit the `index.php` file wihtin the `.well-know/webfinger` folder and set the correct path for you timeline installation in `$timeline_dir` variable.
@ -84,6 +84,8 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## 🐞 Bugs to fix ## 🐞 Bugs to fix
- [x] (2024-11-30) Fix issues with parsing markdown vs. twtxt syntax (replaceed slimdown with Parsedown, supporting lists, block quotes, code/blocks, links, images) - [x] (2024-11-30) Fix issues with parsing markdown vs. twtxt syntax (replaceed slimdown with Parsedown, supporting lists, block quotes, code/blocks, links, images)
- [x] (2024-12-26) Extend session duration for 30 days
- [ ] (2024-12-26) Read the config.ini in a centralized place and add validations useful when installing or upgrading `timeline`.
## 🚀 Features to code ## 🚀 Features to code
@ -97,7 +99,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# 🙏 Credits / shoutouts # 🙏 Credits / shoutouts
## Ideas and inspiration ## Ideas and inspiration
- [twtxt](https://twtxt.readthedocs.io) - The original decentralised, minimalist microblogging service for hackers - [twtxt](https://twtxt.readthedocs.io) - The original decentralised, minimalist microblogging service for hackers
@ -109,7 +111,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- [twtxt-php](https://github.com/eapl-gemugami/twtxt-php) - A minimalistic and personal PHP site for your twtxt microblogging. - [twtxt-php](https://github.com/eapl-gemugami/twtxt-php) - A minimalistic and personal PHP site for your twtxt microblogging.
- [Slimdown](https://github.com/jbroadway/slimdown) - A simple regex-based Markdown parser in PHP. - [Slimdown](https://github.com/jbroadway/slimdown) - A simple regex-based Markdown parser in PHP.
- Tag cloud feature is based on php code by [Domingos Faria](https://social.dfaria.eu/search) - Tag cloud feature is based on php code by [Domingos Faria](https://social.dfaria.eu/search)

View file

@ -1 +1 @@
2024.12.06 2024.12.26

View file

@ -10,9 +10,9 @@ if ($config['debug_mode']) {
require_once('session.php'); require_once('session.php');
if (!isset($_SESSION['valid_session'])) { if (!isset($_SESSION['valid_session'])) {
$secretKey = $config['totp_secret']; $secretKey = $config['totp_secret'];
$cookieVal = decodeCookie($secretKey); $cookieVal = isSavedCookieValid($secretKey);
if ($cookieVal === false) { # Valid cookie ? if ($cookieVal === false) { # Valid cookie ?
header('Location: login.php'); header('Location: login.php');
@ -56,27 +56,30 @@ if (isset($_POST['submit'])) {
exit; exit;
} }
} else { ?> } else { ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8"> <head>
<title>twtxt</title> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> <title>twtxt</title>
<link rel="stylesheet" type="text/css" href="style.css"> <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
</head> <link rel="stylesheet" type="text/css" href="style.css">
<body> </head>
<h1><a href=".">twtxt</a></h1>
<form method="POST" class="column"> <body>
<div id="login"> <h1><a href=".">twtxt</a></h1>
<?php if ($invalidURL) { ?> <form method="POST" class="column">
<div class="alert">URL is invalid, check it!</div><br> <div id="login">
<?php } ?> <?php if ($invalidURL) { ?>
<label for="fname">URL to twtxt.txt file</label> <div class="alert">URL is invalid, check it!</div><br>
<br> <?php } ?>
<input type="text" id="url" name="url" class="input" autocomplete="off"><br> <label for="fname">URL to twtxt.txt file</label>
<input type="submit" value="Add URL" class="btn"> <br>
</div> <input type="text" id="url" name="url" class="input" autocomplete="off"><br>
</form> <input type="submit" value="Add URL" class="btn">
</body> </div>
</html> </form>
</body>
</html>
<?php } ?> <?php } ?>

View file

@ -1,101 +0,0 @@
<?php
$config = parse_ini_file('private/config.ini');
const COOKIE_NAME = 'remember_user';
const ENCRYPTION_METHOD = 'aes-256-cbc';
session_start([
'name' => '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;
}

View file

@ -1,4 +1,4 @@
<?php <?php
function getBaseURI() // https://github.com/taniarascia/comments/issues/26#issuecomment-1458121921 function getBaseURI() // https://github.com/taniarascia/comments/issues/26#issuecomment-1458121921
{ {
@ -30,26 +30,28 @@ $routes = [
'/new' => 'new_twt.php', '/new' => 'new_twt.php',
'/add' => 'add_feed.php', '/add' => 'add_feed.php',
'/following' => 'following.php', '/following' => 'following.php',
//'/refresh' => 'load_twt_files.php',
'/refresh' => 'refresh.php', '/refresh' => 'refresh.php',
'/login' => 'login.php', '/login' => 'login.php',
'/logout' => 'logout.php', '/logout' => 'logout.php',
'/profile' => 'profile.php', '/profile' => 'profile.php',
'/replies' => 'replies.php', '/replies' => 'replies.php',
'/gallery' => 'gallery.php', '/gallery' => 'gallery.php',
//'/profile/([a-zA-Z0-9_-]+)' => 'profile.php', '/conv/([a-zA-Z0-9]{7})' => 'conv.php', // matches only twtHash of exactly 7 alphanumeric characters
'/conv/([a-zA-Z0-9]{7})' => 'conv.php', // matches only twtHash of exactly 7 alphanumeric characters '/post/([a-zA-Z0-9]{7})' => 'post.php', // matches only twtHash of exactly 7 alphanumeric characters
'/post/([a-zA-Z0-9]{7})' => 'post.php', // matches only twtHash of exactly 7 alphanumeric characters
//'/thumb' => 'thumb.php',
'/upload' => 'upload_img.php', '/upload' => 'upload_img.php',
'/webmention' => 'webmention_endpoint.php', '/webmention' => 'webmention_endpoint.php',
//'/thumb' => 'thumb.php',
//'/profile/([a-zA-Z0-9_-]+)' => 'profile.php',
# Debug endpoints
'/test_login' => 'test_login.php',
]; ];
// Loop through the defined routes and try to match the request URI // Loop through the defined routes and try to match the request URI
foreach ($routes as $pattern => $action) { foreach ($routes as $pattern => $action) {
if (preg_match('#^' . $pattern . '$#', $path, $matches)) { if (preg_match('#^' . $pattern . '$#', $path, $matches)) {
// Extract any matched parameters (e.g., username) // Extract any matched parameters (e.g., username)
if(!empty($matches[1])) { if(!empty($matches[1])) {
//array_shift($matches); //array_shift($matches);
$id = $matches[1]; $id = $matches[1];

138
libs/persistent_session.php Normal file
View file

@ -0,0 +1,138 @@
<?php
$config = parse_ini_file('private/config.ini');
# TODO: Move this verification to another file
$required_keys = ['secret_key', 'password', 'totp_secret', 'totp_digits', 'secure_cookies'];
$missing_keys = array_filter($required_keys, fn($key) => !isset($config[$key]));
if (!empty($missing_keys)) {
die('Missing required keys in config.ini: ' . implode(', ', $missing_keys));
}
if (strlen($config['secret_key']) < 32) {
die('Secret key in config.ini must be at least 32 characters long');
}
const COOKIE_NAME = 'timeline_login';
const ENCRYPTION_METHOD = 'aes-256-cbc';
const EXPIRATION_DAYS = 30;
const HASH_LENGTH = 128;
const HASH_ALGORITHM = 'sha512';
session_start([
'name' => 'timeline_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 < 7.3
]);
function hasValidSession(): bool {
# If short lived session is valid
if (isset($_SESSION['session_expiration']) && $_SESSION['session_expiration'] > time()) {
return true;
}
# TODO: Check if the session has expired
# Add more protection to prevent session fixation
# https://owasp.org/www-community/attacks/Session_fixation
# Otherwise, check the persistent cookie
return isSavedCookieValid();
}
function getCookieData() {
if (!isset($_COOKIE[COOKIE_NAME])) {
#echo "Cookie " . COOKIE_NAME . " not found";
return false;
}
$raw = base64_decode($_COOKIE[COOKIE_NAME]);
#var_dump($raw);
# Cookie should be at least the size of the hash length.
# If it's not, we can just bail out
if (strlen($raw) < HASH_LENGTH) {
#echo "Didn't get minimum length";
return false;
}
$config = parse_ini_file('private/config.ini');
# The cookie data contains the actual data w/ the hash concatenated to the end,
# since the hash is a fixed length, we can extract the last hash_length chars
# to get the hash.
$hash = substr($raw, strlen($raw) - HASH_LENGTH, HASH_LENGTH);
$data = substr($raw, 0, - (HASH_LENGTH));
# Calculate the expected hash from the data. If the data has not been
# tampered with, $hash and $hash_calculated will be the same
$hash_calculated = hash_hmac(HASH_ALGORITHM, $data, $config['secret_key']);
if ($hash_calculated !== $hash) {
#echo "Different HASH. Tempered data?";
return False;
}
if (intval($data) < time()) {
#echo "Cookie expired";
return False;
}
return $data;
}
function makePersistentCookie() {
$config = parse_ini_file('private/config.ini');
$cookieExpiry = EXPIRATION_DAYS * 24 * 60 * 60 + time(); # X days
#$cookieExpiry = 10 + time(); # Debug value - 10 seconds
# Calculate a hash for the data and append it to the end of the data string
$cookieValue = strval($cookieExpiry);
$hash = hash_hmac(HASH_ALGORITHM, $cookieValue, $config['secret_key']);
$cookieValue .= $hash;
$cookieValue = base64_encode($cookieValue);
# Also create the short-timed session
$_SESSION['session_expiration'] = $cookieExpiry;
return setcookie(COOKIE_NAME, $cookieValue, [
'expires' => $cookieExpiry,
'secure' => $config['secure_cookies'],
'httponly' => true,
'samesite' => 'Strict',
]);
}
function saveLogin() {
makePersistentCookie();
}
function isSavedCookieValid() {
$cookieExpiry = getCookieData();
if ($cookieExpiry === false) {
deletePersistentCookie();
return false;
}
# @eapl As it's implemented, the user has to login again in 30 days
# since the first login, which I think is a good compromise.
# Refresh session
$_SESSION['session_expiration'] = intval($cookieExpiry);
return true;
}
function deletePersistentCookie() {
if (isset($_COOKIE[COOKIE_NAME])) {
unset($_COOKIE[COOKIE_NAME]);
setcookie(COOKIE_NAME, '', time() - 3600);
}
}

View file

@ -1,30 +1,36 @@
<?php <?php
require_once('libs/TOTP.php'); require_once 'libs/TOTP.php';
require_once 'libs/persistent_session.php';
$config = parse_ini_file('private/config.ini'); $config = parse_ini_file('private/config.ini');
$password = $config['password']; $passwordInConfig = $config['password'];
session_start(); function checkValidSessionOrRedirectToLogin() {
if (!hasValidSession()) {
if (isset($_POST['submit_pass']) && $_POST['pass']) header('Location: ./login');
{ exit();
$pass = $_POST['pass'];
// @eapl.me 2023-11-23 - I'm trying to add support to passwords
// and TOTP (passwordless). So, in the Pwd field you can enter
// the password, or the current TOTP
if ($pass == $password)
{
$_SESSION['password'] = $pass;
}
elseif ($isCodeValid = verifyTOTP(
$config['totp_secret'], $pass, intval($config['totp_digits']))
)
{
// If TOTP is valid, assume that we entered the Password
$_SESSION['password'] = $password;
}
else
{
$error = "Incorrect Password";
} }
} }
if (isset($_POST['submit_pass']) && $_POST['pass']) {
$passwordInForm = $_POST['pass'];
if ($passwordInForm == $passwordInConfig) {
$_SESSION['password'] = $passwordInForm;
saveLogin();
} elseif ($isCodeValid = verifyTOTP(
$config['totp_secret'],
$passwordInForm,
intval($config['totp_digits'])
)) {
$_SESSION['password'] = 'valid_totp';
saveLogin();
} else {
$error = 'Incorrect Password';
}
}
# Check for an empty password
if (isset($_POST['submit_pass']) && !$_POST['pass']) {
$error = 'Type a password';
}

View file

@ -16,6 +16,7 @@ class TwtxtFile {
public $mainURL = ''; // First found URL public $mainURL = ''; // First found URL
public $URLs = []; public $URLs = [];
public $nick = ''; public $nick = '';
public $domain = '';
public $avatar = ''; public $avatar = '';
public $emoji = ''; public $emoji = '';
public $description = ''; public $description = '';
@ -37,6 +38,7 @@ class Twt {
public $avatar; public $avatar;
public $emoji; public $emoji;
public $nick; public $nick;
public $domain;
public $mainURL; public $mainURL;
public $images = []; public $images = [];
public $tags = []; public $tags = [];
@ -456,8 +458,7 @@ function getTwtsFromTwtxtString($url) {
} }
} }
// Clean up nick if set to something like `@soren@darch.dk` instead of just `soren` // Clean up nick if set to something like `@soren@darch.dk` instead of just `soren` - mosty for (re)feeds from Mastodon etc.
// mosty for (re)feeds from Mastodon etc.
if (str_contains($twtxtData->nick, "@")) { if (str_contains($twtxtData->nick, "@")) {
$str = $twtxtData->nick; $str = $twtxtData->nick;
$str = ltrim($str, "@"); $str = ltrim($str, "@");
@ -465,17 +466,25 @@ function getTwtsFromTwtxtString($url) {
} }
// Fallback for nick and url if not set in twtxt.txt // Fallback for nick and url if not set in twtxt.txt
// TODO: Use nick from local follow list as fallback?
if ($twtxtData->nick === "") { if ($twtxtData->nick === "") {
$str = parse_url($url, PHP_URL_HOST); $str = parse_url($url, PHP_URL_HOST);
$str = str_replace("www.", "", $str); $str = str_replace("www.", "", $str);
$str = explode(".", $str)[0]; // take the first [0] from splitting the host at "." //$str = explode(".", $str)[0]; // take the first [0] from splitting the host at "."
$twtxtData->nick = $str; $twtxtData->nick = $str;
} }
if ($twtxtData->mainURL === "") { if ($twtxtData->mainURL === "") {
$twtxtData->mainURL = $url; $twtxtData->mainURL = $url;
} }
// Use only nick as handle if nick and domain is the same
$twtxtData->domain = parse_url($twtxtData->mainURL, PHP_URL_HOST);
if ($twtxtData->nick === $twtxtData->domain) {
$twtxtData->domain = "";
} else {
$twtxtData->domain = "@".$twtxtData->domain;
}
if (!str_starts_with($currentLine, '#')) { if (!str_starts_with($currentLine, '#')) {
$explodedLine = explode("\t", $currentLine); $explodedLine = explode("\t", $currentLine);
@ -550,6 +559,7 @@ function getTwtsFromTwtxtString($url) {
$twt->emoji = $twtxtData->emoji; $twt->emoji = $twtxtData->emoji;
$twt->nick = $twtxtData->nick; $twt->nick = $twtxtData->nick;
$twt->mainURL = $twtxtData->mainURL; $twt->mainURL = $twtxtData->mainURL;
$twt->domain = $twtxtData->domain;
$twtxtData->twts[$timestamp] = $twt; $twtxtData->twts[$timestamp] = $twt;
} }

View file

@ -1,43 +1,43 @@
<!-- List UI --> <!-- List UI -->
<form action="" method="get"> <form action="" method="get">
<!-- Select a list: --> <!-- Select a list: -->
<select name="list" onchange="this.form.submit()"> <select name="list" onchange="this.form.submit()">
<option value="twtxt.txt" selected>twtxt.txt (Main)</option> <option value="twtxt.txt" selected>twtxt.txt (Main)</option>
<?php <?php
// TODO: fix it so if List -> Selected for both public and private lists // TODO: fix it so if List -> Selected for both public and private lists
if( isset($_SESSION['password'])) { if (isset($_SESSION['password'])) {
if($_SESSION['password']=="$password") { // Hacky login if ($_SESSION['password'] == "$passwordInConfig") { // Hacky login
// Private lists // Private lists
echo "<option disabled>Private Lists:</option>"; echo "<option disabled>Private Lists:</option>";
foreach (glob("private/twtxt-*.txt") as $filename) { foreach (glob("private/twtxt-*.txt") as $filename) {
if($filename == $_GET['lists']) $attr="selected"; if ($filename == $_GET['lists']) $attr = "selected";
else $attr = ""; else $attr = "";
$listName = $filename; $listName = $filename;
$listName = str_replace("private/twtxt-", "", $listName); $listName = str_replace("private/twtxt-", "", $listName);
$listName = str_replace("_", " ", $listName); $listName = str_replace("_", " ", $listName);
$listName = str_replace(".txt", "", $listName); $listName = str_replace(".txt", "", $listName);
echo "<option value='{$filename}' {$attr}>$listName</option>"; echo "<option value='{$filename}' {$attr}>$listName</option>";
} }
// Public Lists // Public Lists
echo "<option disabled>Public Lists:</option>"; echo "<option disabled>Public Lists:</option>";
}
} }
}
foreach (glob("twtxt-*.txt") as $filename) { foreach (glob("twtxt-*.txt") as $filename) {
if($filename == $_GET['lists']) $attr="selected"; if ($filename == $_GET['lists']) $attr = "selected";
else $attr = ""; else $attr = "";
$listName = $filename; $listName = $filename;
$listName = str_replace("twtxt-", "", $listName); $listName = str_replace("twtxt-", "", $listName);
$listName = str_replace("_", " ", $listName); $listName = str_replace("_", " ", $listName);
$listName = str_replace(".txt", "", $listName); $listName = str_replace(".txt", "", $listName);
echo "<option value='{$filename}' {$attr}>$listName</option>"; echo "<option value='{$filename}' {$attr}>$listName</option>";
} }
?> ?>
</select> </select>
<noscript><button type="submit">View list</button></noscript> <noscript><button type="submit">View list</button></noscript>
</form> </form>

View file

@ -1,43 +1,43 @@
<!-- List UI --> <!-- List UI -->
<form action="" method="get"> <form action="" method="get">
<!-- Select a list: --> <!-- Select a list: -->
<select name="list" onchange="this.form.submit()"> <select name="list" onchange="this.form.submit()">
<option value="twtxt.txt" selected>twtxt.txt (Main)</option> <option value="twtxt.txt" selected>twtxt.txt (Main)</option>
<?php <?php
// TODO: fix it so if List -> Selected for both public and private lists // TODO: fix it so if List -> Selected for both public and private lists
if( isset($_SESSION['password'])) { if (isset($_SESSION['password'])) {
if($_SESSION['password']=="$password") { // Hacky login if ($_SESSION['password'] == "$passwordInConfig") { // Hacky login
// Private lists // Private lists
echo "<option disabled>Private Lists:</option>"; echo "<option disabled>Private Lists:</option>";
foreach (glob("private/twtxt-*.txt") as $filename) { foreach (glob("private/twtxt-*.txt") as $filename) {
if($filename == $_GET['list']) $attr="selected"; if ($filename == $_GET['list']) $attr = "selected";
else $attr = ""; else $attr = "";
$listName = $filename; $listName = $filename;
$listName = str_replace("private/twtxt-", "", $listName); $listName = str_replace("private/twtxt-", "", $listName);
$listName = str_replace("_", " ", $listName); $listName = str_replace("_", " ", $listName);
$listName = str_replace(".txt", "", $listName); $listName = str_replace(".txt", "", $listName);
echo "<option value='{$filename}' {$attr}>$listName</option>"; echo "<option value='{$filename}' {$attr}>$listName</option>";
} }
// Public Lists // Public Lists
echo "<option disabled>Public Lists:</option>"; echo "<option disabled>Public Lists:</option>";
}
} }
}
foreach (glob("twtxt-*.txt") as $filename) { foreach (glob("twtxt-*.txt") as $filename) {
if($filename == $_GET['list']) $attr="selected"; if ($filename == $_GET['list']) $attr = "selected";
else $attr = ""; else $attr = "";
$listName = $filename; $listName = $filename;
$listName = str_replace("twtxt-", "", $listName); $listName = str_replace("twtxt-", "", $listName);
$listName = str_replace("_", " ", $listName); $listName = str_replace("_", " ", $listName);
$listName = str_replace(".txt", "", $listName); $listName = str_replace(".txt", "", $listName);
//$filename = "TODO".$baseURL."/".$filename; //$filename = "TODO".$baseURL."/".$filename;
echo "<option value='{$filename}' {$attr}>$listName</option>"; echo "<option value='{$filename}' {$attr}>$listName</option>";
} }
?> ?>
</select> </select>
<noscript><button type="submit">View list</button></noscript> <noscript><button type="submit">View list</button></noscript>
</form> </form>

View file

@ -63,7 +63,7 @@ if ($is_gallery) {
<div> <div>
<a href="<?=$profileURL?>" class="author"> <a href="<?=$profileURL?>" class="author">
<strong><?=$profile->nick?></strong>@<?=parse_url($profile->mainURL, PHP_URL_HOST);?> <strong>@<?=$profile->nick?></strong><?=$profile->domain?>
</a> </a>
<p><?=$profile->description?></p> <p><?=$profile->description?></p>

View file

@ -15,7 +15,7 @@
</a> </a>
<div> <div>
<a href="<?=$baseURL?>/profile?url=<?=$twt->mainURL?>" class="author"> <a href="<?=$baseURL?>/profile?url=<?=$twt->mainURL?>" class="author">
<strong><?=$twt->nick?></strong>@<?=parse_url($twt->mainURL, PHP_URL_HOST);?> <strong>@<?=$twt->nick?></strong><?=$twt->domain?>
</a> </a>
<div class="twt-msg"> <div class="twt-msg">
@ -39,7 +39,7 @@
if (isset($_SESSION['password'])) { if (isset($_SESSION['password'])) {
echo ' | <a href="' . $baseURL . '/new?hash=' . $twt->hash . '">Reply</a>'; echo ' | <a href="' . $baseURL . '/new?hash=' . $twt->hash . '">Reply</a>';
} }
?> ?>
<!-- (<a href="new_twt.php?hash=<?=$twt->hash?>">via email</a>) TODO: mailto-link --> <!-- (<a href="new_twt.php?hash=<?=$twt->hash?>">via email</a>) TODO: mailto-link -->
@ -49,9 +49,8 @@
</article> </article>
<?php } <?php }
require_once 'libs/session.php';
if (!isset($_SESSION['password'])) { if (!hasValidSession()) {
echo '<center><a href="mailto:' . $config['email'] . '?subject=RE: ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . '" class="button">Comment via email</a></center>'; echo '<center><a href="mailto:' . $config['email'] . '?subject=RE: ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . '" class="button">Comment via email</a></center>';
} }
?>

View file

@ -1,8 +1,8 @@
<?php <?php
require_once "libs/twtxt.php"; require_once "libs/twtxt.php";
// Send webmentions (TODO: move to it own file?) // Send webmentions
$new_mentions = getMentionsFromTwt($twt); $new_mentions = getMentionsFromTwt($twt);
foreach ($new_mentions as $mention) { foreach ($new_mentions as $mention) {
//print_r(getMentionsFromTwt($twt)); //print_r(getMentionsFromTwt($twt));

View file

@ -47,7 +47,15 @@ webmentions_txt_path = "./mentions.txt"
public_webmentions = "https://example.com/timeline/mentions.txt" public_webmentions = "https://example.com/timeline/mentions.txt"
[security] [security]
; Generate it with the TOTP module ; Secret key to encrypt cookies of at least 256-bit (32 characters)
; Create one here: https://randomkeygen.com (CodeIgniter Encryption Keys)
secret_key = ""
; Simple password
password = "change_me"
; A dynamic password (TOTP) changing every 30 seconds
; Use a TOTP client with support for 10 digits like Aegis (Android)
totp_digits = 10 totp_digits = 10
totp_secret = "1234567890" totp_secret = "1234567890"

View file

@ -8,16 +8,13 @@ require_once('libs/twtxt.php');
require_once('libs/hash.php'); require_once('libs/hash.php');
*/ */
require_once("partials/base.php"); require_once "partials/base.php";
require_once "libs/session.php";
checkValidSessionOrRedirectToLogin();
$config = parse_ini_file('private/config.ini'); $config = parse_ini_file('private/config.ini');
if (!isset($_SESSION['password'])) {
header('Location: ./login');
exit();
}
$max_execution_time = intval($config['max_execution_time']); $max_execution_time = intval($config['max_execution_time']);
if ($max_execution_time < 1) { if ($max_execution_time < 1) {
$max_execution_time = 1; $max_execution_time = 1;
@ -69,15 +66,15 @@ foreach ($fileLines as $currentLine) {
$i = 1; $i = 1;
$total = count($twtFollowingList); $total = count($twtFollowingList);
foreach ($twtFollowingList as $following) { foreach ($twtFollowingList as $following) {
$float = $i/$total; $float = $i/$total;
$percent = intval($float * 100)."%"; $percent = intval($float * 100)."%";
// Javascript for updating the progress bar and information // Javascript for updating the progress bar and information
echo '<script language="javascript"> echo '<script language="javascript">
document.getElementById("refreshLabel").innerHTML = "Updating: '.$following[1].' ('.$i.'/'.$total.')"; document.getElementById("refreshLabel").innerHTML = "Updating: '.$following[1].' ('.$i.'/'.$total.')";
document.getElementById("refreshProgress").value = "'.$float.'"; document.getElementById("refreshProgress").value = "'.$float.'";
document.getElementById("refreshProgress").innerHTML = "'.$percent.'"; document.getElementById("refreshProgress").innerHTML = "'.$percent.'";
</script>'; </script>';
updateCachedFile($following[1]); updateCachedFile($following[1]);

View file

@ -1,6 +1,9 @@
<?php <?php
require_once('partials/base.php'); require_once 'partials/base.php';
require_once('partials/webfinger_lookup.php'); require_once 'partials/webfinger_lookup.php';
require_once 'libs/session.php';
checkValidSessionOrRedirectToLogin();
// TODO: Give a warning if the file is not found // TODO: Give a warning if the file is not found
$config = parse_ini_file('private/config.ini'); $config = parse_ini_file('private/config.ini');
@ -13,18 +16,6 @@ if ($config['debug_mode']) {
$txt_file_path = $config['txt_file_path']; $txt_file_path = $config['txt_file_path'];
if (!isset($_SESSION['password'])) {
header('Location: ./login');
exit();
}
/*
if (!has_valid_session()) {
header('Location: login.php');
exit();
}
*/
if (isset($_POST['url'])) { if (isset($_POST['url'])) {
$url = trim(filter_input(INPUT_POST, 'url')); $url = trim(filter_input(INPUT_POST, 'url'));
$nick = trim(filter_input(INPUT_POST, 'nick')); $nick = trim(filter_input(INPUT_POST, 'nick'));
@ -67,36 +58,36 @@ if (isset($_POST['url'])) {
exit; exit;
} else { ?> } else { ?>
<?php <?php
$title = "Add feed - ".$title; $title = "Add feed - " . $title;
include 'partials/header.php'; include 'partials/header.php';
?> ?>
<h2>Webfinger lookup</h2> <h2>Webfinger lookup</h2>
<form method="post" action=""> <form method="post" action="">
<label>Check if a webfinger handle has a link to a twtxt.txt feed</label> <label>Check if a webfinger handle has a link to a twtxt.txt feed</label>
<input type="text" name="webfinger" size="50" autocomplete="off" required placeholder="name@example.com" value="<?= $wf_request; ?>"> <input type="text" name="webfinger" size="50" autocomplete="off" required placeholder="name@example.com" value="<?= $wf_request; ?>">
<br>
<input type="submit" name="submit" value="Lookup"><br>
</form>
<?= $wf_error; ?>
<h1>Add a new feed to follow</h1>
<form method="POST" class="column">
<div id="follow">
<label for="nick">Nick</label>
<input type="text" id="nick" name="nick" class="input" size="50" autocomplete="off" required value="<?= $wf_nick; ?>">
<label for="url">URL to follow</label>
<input type="url" id="url" name="url" class="input" size="50" autocomplete="off" required value="<?= $wf_url; ?>">
<br> <br>
<input type="submit" value="Follow" class="btn"> <input type="submit" name="submit" value="Lookup"><br>
</div> </form>
</form>
<!-- PHP: GET FOOTER --><?php include 'partials/footer.php';?> <?= $wf_error; ?>
<?php } ?> <h1>Add a new feed to follow</h1>
<form method="POST" class="column">
<div id="follow">
<label for="nick">Nick</label>
<input type="text" id="nick" name="nick" class="input" size="50" autocomplete="off" required value="<?= $wf_nick; ?>">
<label for="url">URL to follow</label>
<input type="url" id="url" name="url" class="input" size="50" autocomplete="off" required value="<?= $wf_url; ?>">
<br>
<input type="submit" value="Follow" class="btn">
</div>
</form>
<!-- PHP: GET FOOTER --><?php include 'partials/footer.php'; ?>
<?php } ?>

View file

@ -1,7 +1,7 @@
<?php <?php
require_once("partials/base.php"); require_once("partials/base.php");
$title = "Following - ".$title; $title = "Following - " . $title;
include 'partials/header.php'; include 'partials/header.php';
@ -18,46 +18,48 @@ include 'partials/header.php';
<!-- <th></th> --> <!-- <th></th> -->
<th>Nick</th> <th>Nick</th>
<th>URL</th> <th>URL</th>
<?php if(isset($_SESSION['password']) && $_SESSION['password']=="$password") { ?> <?php if (isset($_SESSION['password']) && $_SESSION['password'] == "$passwordInConfig") { ?>
<th>Time ago</th> <th>Time ago</th>
<?php } ?> <?php } ?>
</tr> </tr>
<?php foreach ($twtFollowingList as $currentFollower) { ?> <?php foreach ($twtFollowingList as $currentFollower) { ?>
<tr> <tr>
<!-- <td></td> --> <!-- <td></td> -->
<td><a href="<?= $baseURL ?>/profile?url=<?= $currentFollower[1] ?>"><?= $currentFollower[0] ?></a></td> <td><a href="<?= $baseURL ?>/profile?url=<?= $currentFollower[1] ?>"><?= $currentFollower[0] ?></a></td>
<!-- <td><a href="/?twt=<?= $currentFollower[1] ?>"><?= $currentFollower[0] ?></a></td> --> <!-- <td><a href="/?twt=<?= $currentFollower[1] ?>"><?= $currentFollower[0] ?></a></td> -->
<td><?= $currentFollower[1] ?> <td><?= $currentFollower[1] ?>
<!-- <?php //if ($validSession) { ?> --> <!-- <?php //if ($validSession) {
<!-- <a href="?remove_url=<?= $currentFollower[1] ?>">Remove</a> --> ?> -->
<!-- <?php // } ?> --> <!-- <a href="?remove_url=<?= $currentFollower[1] ?>">Remove</a> -->
</td> <!-- <?php // }
<?php if(isset($_SESSION['password']) && $_SESSION['password']=="$password") { ?> ?> -->
<td> </td>
<?php <?php if (isset($_SESSION['password']) && $_SESSION['password'] == "$passwordInConfig") { ?>
// Test first if URL is a valid feed: <td>
if (is_array(getTwtsFromTwtxtString($currentFollower[1])->twts)) { <?php
// Test first if URL is a valid feed:
if (is_array(getTwtsFromTwtxtString($currentFollower[1])->twts)) {
// Then test if latest twt is at start or end of file: // Then test if latest twt is at start or end of file:
$resetVar = reset(getTwtsFromTwtxtString($currentFollower[1])->twts); $resetVar = reset(getTwtsFromTwtxtString($currentFollower[1])->twts);
$endVar = end(getTwtsFromTwtxtString($currentFollower[1])->twts); $endVar = end(getTwtsFromTwtxtString($currentFollower[1])->twts);
if ($resetVar->timestamp < $endVar->timestamp) { // TODO: this can be swapped to get time of first twt if ($resetVar->timestamp < $endVar->timestamp) { // TODO: this can be swapped to get time of first twt
echo $endVar->displayDate; echo $endVar->displayDate;
} else { } else {
echo $resetVar->displayDate; echo $resetVar->displayDate;
}
} }
} ?>
?>
</td> </td>
<?php } ?> <?php } ?>
</tr> </tr>
<?php } ?> <?php } ?>
</table> </table>
</center> </center>
<!-- FOOTER --><?php include 'partials/footer.php';?> <!-- FOOTER --><?php include 'partials/footer.php'; ?>

View file

@ -18,25 +18,23 @@ if (!empty($_GET['profile'])) { // Show twts for some user (Profile view)
// Load twts, taking $paginateTwts into consideration // Load twts, taking $paginateTwts into consideration
require_once 'partials/base.php'; require_once 'partials/base.php';
require_once 'libs/session.php';
$title = "Timeline for ".$title; $title = "Timeline for $title";
// Redirect guests to Profile view, if URL isn't set to home twtxt.txt
// Redirect guests to Profile view, if url not set til home twtxt.txt if (!hasValidSession() && isset($_GET['url'])) {
if ($_GET['url'] != $config['public_txt_url']) {
if (!isset($_SESSION['password']) && (isset($_GET['url']))) { header('Location: ./profile');
if ($_GET['url'] != $config['public_txt_url']) { exit();
header('Location: ./profile'); }
exit();
}
} }
include_once 'partials/header.php'; include_once 'partials/header.php';
if (isset($_SESSION['password'])) { if (hasValidSession()) {
include 'views/new_twt.php'; // TODO: Split up new_twt into a view and a partial include 'views/new_twt.php'; // TODO: Split up new_twt into a view and a partial
} else { } else {
echo '<center><h2>Timeline</h2>'; echo '<center><h2>Timeline</h2>';
echo '<p>Recent posts from feeds followed by <a href="./profile"> echo '<p>Recent posts from feeds followed by <a href="./profile">

View file

@ -1,28 +1,30 @@
<?php <?php
require_once("partials/base.php"); require_once "partials/base.php";
$title = "Login - ".$title; $title = "Login - $title";
// Password comes from libs/session.php // $password comes from libs/session.php
if (isset($_SESSION['password'])) { if (isset($_SESSION['password'])) {
if ($_SESSION['password'] == $password) { if ($_SESSION['password'] == $passwordInConfig) {
header("Location: ."); header("Location: .");
include 'partials/header.php'; die();
die();
}
} }
} else {
else { include 'partials/header.php';
include 'partials/header.php';
?> ?>
<center> <!-- TODO: Replace center and font tags with CSS -->
<h2>Enter password or TOTP</h2> <center>
<form method="post" action="" id="login_form"> <h2>Enter password or TOTP</h2>
<input type="password" name="pass" placeholder="Password" autofocus><br> <form method="post" action="" id="login_form">
<input type="submit" name="submit_pass" value="Login"> <input type="password" name="pass" placeholder="Password" autofocus><br>
<p><font style="color:red;"><?php if(isset($error)) {echo $error;}?></font></p> <input type="submit" name="submit_pass" value="Login">
</form> <p><font style="color:red;">
</center> <?php if (isset($error)) {
echo $error;
} ?>
</font></p>
</form>
</center>
<?php } ?> <?php } ?>
<!-- PHP: GET FOOTER --><?php include 'partials/footer.php';?> <!-- PHP: GET FOOTER --><?php include 'partials/footer.php'; ?>

View file

@ -1,6 +1,7 @@
<?php <?php
require_once "libs/persistent_session.php";
session_start(); deletePersistentCookie();
session_unset(); session_unset();
session_destroy(); session_destroy();

View file

@ -1,4 +1,7 @@
<?php <?php
require_once 'libs/session.php';
checkValidSessionOrRedirectToLogin();
// TODO: Give a warning if the file is not found // TODO: Give a warning if the file is not found
$config = parse_ini_file('private/config.ini'); $config = parse_ini_file('private/config.ini');
@ -10,16 +13,10 @@ if ($config['debug_mode']) {
$txt_file_path = $config['txt_file_path']; $txt_file_path = $config['txt_file_path'];
$public_txt_url = $config['public_txt_url']; $public_txt_url = $config['public_txt_url'];
$timezone = $config['timezone']; $timezone = $config['timezone'];
require_once 'libs/session.php';
require_once 'libs/load_timezone.php'; require_once 'libs/load_timezone.php';
if (!isset($_SESSION['password'])) {
header('Location: ./login');
exit();
}
if (isset($_POST['submit'])) { if (isset($_POST['submit'])) {
$new_post = filter_input(INPUT_POST, 'new_post'); $new_post = filter_input(INPUT_POST, 'new_post');
$new_post = trim($new_post); $new_post = trim($new_post);

View file

@ -1,10 +1,8 @@
<?php <?php
require_once "partials/base.php"; require_once "partials/base.php";
require_once 'libs/session.php';
if (!isset($_SESSION['password'])) { checkValidSessionOrRedirectToLogin();
header('Location: ./login');
exit();
}
ob_start(); ob_start();

14
views/test_login.php Normal file
View file

@ -0,0 +1,14 @@
<?php
# A simple text to check if sessions are working OK
# Remove it from index.php to hide it from the public
require_once "libs/persistent_session.php";
if (!hasValidSession()) {
echo "Not a valid session - Go to /login";
#header("Location: /login");
exit;
}
var_dump($_SESSION);
echo "Valid session";

View file

@ -1,20 +1,14 @@
<?php <?php
require_once "partials/base.php";
require_once 'libs/session.php';
require_once("partials/base.php"); checkValidSessionOrRedirectToLogin();
if (!isset($_SESSION['password'])) { $title = "Upload - $title";
header('Location: ./login');
die();
}
$title = "Upload - ".$title;
include_once 'partials/header.php'; include_once 'partials/header.php';
if (!empty($_POST)) { if (!empty($_POST)) {
// Based on code from: https://www.w3schools.com/php/php_file_upload.asp // Based on code from: https://www.w3schools.com/php/php_file_upload.asp
//echo getcwd() ."<br>"; //echo getcwd() ."<br>";