forgot to push whoops

This commit is contained in:
Bye 2023-11-06 16:38:18 +00:00
parent 6b61e9621d
commit 5a063e661c
22 changed files with 699 additions and 96 deletions

132
account.php Normal file
View file

@ -0,0 +1,132 @@
<?php
if (!isset($_SESSION['auth'])) {
header('Location: /signin?callback=/account');
exit;
}
function get_gravatar_url( $email ) {
// Trim leading and trailing whitespace from
// an email address and force all characters
// to lower case
$address = strtolower( trim( $email ) );
// Create an SHA256 hash of the final string
$hash = hash( 'sha256', $address );
// Grab the actual image URL
return 'https://www.gravatar.com/avatar/' . $hash;
}
$stmt = $pdo->prepare('SELECT * FROM accounts WHERE id = ? LIMIT 1');
$stmt->execute([$_SESSION['id']]);
$user = $stmt->fetch();
if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (isset($_POST["old_password"]) && $_POST["old_password"] != "") {
// means password reset is wanted.
if (!password_verify($_POST["old_password"], $user["password"])) {
$password_error = "Incorrect password. (Error 901)";
}
if (password_verify($_POST['new_password'], $user["password"])) {
$password_error = "New password may not be same as old password. (Error 902)";
}
if ($_POST['new_password'] != $_POST['repeat_new_password']) {
$password_error = "The passwords must match. (Error 900)";
}
if (isset($password_error)) {
$message = $password_error;
goto skip_submit;
}
$new_password = password_hash($_POST["new_password"], PASSWORD_DEFAULT);
$sql = "UPDATE accounts SET password = ? WHERE id = ?";
$pdo->prepare($sql)->execute([$new_password, $user["id"]]);
}
if (isset($_POST["display_name"])) {
$sql = "UPDATE accounts SET display_name = ? WHERE id = ?";
$pdo->prepare($sql)->execute([$_POST["display_name"], $user["id"]]);
}
$message = "Updated sucessfully. Changes might take a few minutes to take effect.";
}
skip_submit:
?>
<h1>Your account</h1>
<?php
if (isset($message )) {
echo "<div class='flash'>".$message."</div>";
}
?>
<div id="profile">
<img src="<?= get_gravatar_url($user['email']) ?>">
<div class="details">
<span class="displayname"><?= $user['display_name'] ?></span>
<span class="bcid"><?= format_bcid($user['id']); ?></span>
<time datetime="<?= $user["created_date"] ?>">Since <?= $user["created_date"]; ?></time>
</div>
</div>
<form method="post">
<fieldset>
<legend>Profile</legend>
<div class="container">
<label>BCID</label>
<input type="text" disabled value="<?= format_bcid($user['id']) ?>">
</div>
<div class="container">
<input type="checkbox" disabled checked="<?= $user['verified'] ?>" >
<label> Verified email</label>
</div>
<div class="container">
<label for="email">Email address</label>
<input type="email" name="email" id="email" value="<?= $user['email'] ?>">
</div>
<div class="container">
<label for="display_name">Display name</label>
<input type="text" name="display_name" id="display_name" value="<?= $user['display_name'] ?>">
</div>
</fieldset>
<fieldset>
<legend>Password</legend>
<p>You only need to insert values here if you're resetting your password.</p>
<div class="container">
<label for="old_password">Current password</label>
<input type="password" name="old_password" id="old_password">
</div>
<div class="container">
<label for="new_password">New password</label>
<input type="password" name="new_password" id="new_password">
</div>
<div class="container">
<label for="repeat_new_password">Repeat new password</label>
<input type="password" name="repeat_new_password" id="repeat_new_password">
</div>
</fieldset>
<button class="primary" type="submit"><i class="fa-fw fa-solid fa-floppy-disk"></i> Save</button>
</form>
<div class="dangerzone">
<h2>Danger Zone</h2>
<p><a href="/signout" class="button"><i class="fa-fw fa-solid fa-person-through-window"></i> Sign out</a> <a href="/dangerous/delete_account" class="button danger">Delete account</a></p>
</div>

36
admin_accounts.php Normal file
View file

@ -0,0 +1,36 @@
<?php
if ($_SESSION['id'] != "281G3NV") {
http_response_code(401);
die("<img src='https://http.cat/401.jpg'>");
}
$sql = "SELECT * FROM accounts";
$result = $pdo-> query($sql);
if (!$result) {
http_response_code(500);
die("<img src='https://http.cat/500.jpg'>");
}
$count_req = $pdo->query("SELECT COUNT(*) FROM accounts");
$count = $count_req->fetchColumn();
?>
<h2 class="subheading">Admin</h2>
<h1>Accounts</h1>
<p>There is currently <?= $count ?> accounts registered.</p>
<ul>
<?php
foreach ($result as $row) {
echo "<li><pre>";
print_r($row);
echo "</pre><p><a href='/admin/signinas?id=".$row['id']."'>Sign in as ".$row['display_name']."</a></li>";
}
?>
</ul>

51
admin_initdatabase.php Normal file
View file

@ -0,0 +1,51 @@
<?php
if ($_SESSION['id'] != "281G3NV") {
http_response_code(401);
die("<img src='https://http.cat/401.jpg'>");
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if ($_POST['init'] == 'Init') {
echo("<p>Initialising DB...");
$pdo = new PDO(DB_DSN, DB_USERNAME, DB_PASSWORD, PDO_OPTIONS);
echo "<p>Create table `accounts`";
$stmt = $pdo->prepare('CREATE TABLE `accounts` (
`id` tinytext NOT NULL,
`email` text NOT NULL,,
`display_name` text NULL,
`password` text NOT NULL,
`verified` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;');
try {
$stmt->execute();
} catch (PDOException $e) {
echo('<p>An error occurred: '. $e->getMessage() .'. Will skip. (Most likely the table already exists.)');
}
echo '<p>Set indexes for table `accounts`';
$stmt = $pdo->prepare('ALTER TABLE `accounts`
ADD PRIMARY KEY (`id`(7)),
ADD UNIQUE KEY `email` (`email`) USING HASH;');
try {
$stmt->execute();
} catch (PDOException $e) {
echo('<p>An error occurred: '. $e->getMessage() .'. Most likely this is already set.');
}
echo "<p>Database initialised.</p>";
}
}
?>
<h2 class="subheading">Admin</h2>
<h1>Init database</h1>
<p>Assuming you have the database config configured, you can click this button to create the tables required for this thing to function.</p>
<form method="post">
<button name="init" value="Init" class="primary">Init DB</button>
</form>

0
db.php Normal file
View file

31
forgot_password.php Normal file
View file

@ -0,0 +1,31 @@
<?php
if (isset($_SESSION['auth'])) {
header('Location: /account');
}
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$message = "We've sent an email to that inbox if we find an associated account.";
$sql = "SELECT * FROM accounts WHERE email = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch();
if ($user != null) { // account exists
mail($user['email'], "ByeCorps ID Password Reset Confirmation", "The email was sent!");
}
}
?>
<h1>Forgot password</h1>
<?php if(isset($message)) echo "<p>".$message."</p>"; ?>
<p>Forgot your password? We'll send you an email to reset it.</p>
<form method="post">
<input placeholder="a.dent@squornshellous.cloud" name="email" id="email" type="email">
<button type="submit">Request password reset</button>
</form>

View file

@ -1,5 +1,25 @@
<!-- This is a testing file for the header used on BCID. Copy of header on ByeCorps.com -->
<?php
if (!isset($_SESSION['auth'])) goto skip_auth;
if ($_SESSION['auth']) {
$sql = "SELECT display_name FROM accounts WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_SESSION['id']]);
$name = $stmt->fetchColumn();
}
if ($name == '') {
$name = '<code class=bcid>'.format_bcid($_SESSION['id']).'</code>';
}
skip_auth:
?>
<link rel="stylesheet" href="./styles/global.css">
<link rel="stylesheet" href="./fontawesome/css/all.css">
@ -8,8 +28,12 @@
<a href="/" id="sitetitle"><span class="bc-1">Bye</span><span class="bc-2">Corps</span><span class="bc-3"> ID</span></a></div>
<div class="end">
<?php if (!isset($_SESSION['auth'])) goto signed_out; ?>
<div class="loggedin">
<a href="/account" class="account">Hey there, Bye! <i class="fa-solid fa-fw fa-angle-down"></i></a>
<a href="/account" class="account">Hey there, <?= $name ?>! <i class="fa-solid fa-fw fa-angle-right"></i></a>
</div>
<?php signed_out: ?>
</div>
</header>

3
id.sql
View file

@ -30,7 +30,8 @@ SET time_zone = "+00:00";
CREATE TABLE `accounts` (
`id` tinytext NOT NULL COMMENT 'BCID',
`email` text NOT NULL,
`password` text NOT NULL COMMENT 'Hashed!!!',
`display_name` text NULL,
`password` text NOT NULL,
`verified` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View file

@ -1,6 +1,6 @@
<?php
function ganerate_bcid() {
function generate_bcid() {
$CHARS = str_split("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
return $CHARS[array_rand($CHARS)].$CHARS[array_rand($CHARS)].$CHARS[array_rand($CHARS)].$CHARS[array_rand($CHARS)].$CHARS[array_rand($CHARS)].$CHARS[array_rand($CHARS)].$CHARS[array_rand($CHARS)];
}
@ -16,19 +16,17 @@ function validate_bcid($bcid) {
return 0; // fail condition
}
$BCID = ganerate_bcid();
function format_bcid ($bcid) { // Formats to XXX-XXXX
$stripped_bcid = str_replace([' ','-'], '', $bcid);
$stripped_bcid = strtoupper($stripped_bcid);
echo "<pre>";
echo "Random BCID (unformatted): $BCID
";
echo "Check if BCID is valid: ".validate_bcid($BCID)."
";
if (!validate_bcid($stripped_bcid)) {
throw new Exception('Invalid BCID.');
}
if ($query['bcid']) {
echo "BCID provided in the query: ".$query['bcid']."
";
echo "Checking the BCID provided in the query: ".validate_bcid($query['bcid'])."
";
return substr($stripped_bcid, 0, 3).'-'.substr($stripped_bcid, -4, 4);
}
$BCID = generate_bcid();
?>

View file

@ -3,12 +3,22 @@
session_start();
include("config.php");
include("id_handler.php");
include("time_handler.php");
function does_variable_exists( $variable ) {
return (isset($$variable)) ? "true" : "false";
}
$host_string = $_SERVER['HTTP_HOST'];
$host = explode('.', $host_string);
$uri_string = $_SERVER['REQUEST_URI'];
$query_string = explode('?', $uri_string);
$path = $query_string[0];
if (str_ends_with($path,'/') && $path != "/") {
header('Location: '.substr($path,0, -1));
exit;
}
$uri = array_values(array_filter(explode('/', $uri_string)));
if(isset($query_string[1])) {
@ -21,30 +31,33 @@ if(isset($query_string[1])) {
}
}
else {
$query = array();
$query = array();
}
$pdo = new PDO(DB_DSN, DB_USERNAME, DB_PASSWORD, PDO_OPTIONS);
$include = "404.html";
// routing
if (!$uri) {
// empty array means index
$include = "landing.html";
}
else if ($path == "/signin") {
$doc_title = "Sign in";
include("signin.php");
exit;
}
else if ($path == "/register") {
$doc_title = "Register";
include("register.php");
exit;
}
else if ($path == "/tests/id") {
include("id_handler.php");
exit;
$paths = array(
"/" => ["landing.php"],
"/admin/init/database" => ["admin_initdatabase.php"],
"/admin/accounts" => ["admin_accounts.php"],
"/account" => ["account.php", "Your account"],
"/signin" => ["signin.php", "Sign in"],
"/signup" => ["signup.php", "Sign up"],
"/signout" => ["signout.php", "Signed out"],
"/forgot_password" => ["forgot_password.php", "Forgot password"],
"/admin/signinas" => ["signinas.php"]
);
if (isset($paths[$path])) {
$include = $paths[$path][0];
if (isset($paths[$path][1])) {
$doc_title = $paths[$path][1];
}
}
else {
$doc_title = "404";
http_response_code(404);
@ -60,7 +73,14 @@ else {
<body>
<?php include("header.php"); ?>
<main>
<?php include($include); ?>
<?php
if ($uri[0] == "admin" && $_SESSION['id'] != "281G3NV") {
http_response_code(401);
die("<img src='https://http.cat/401.jpg'>");
}
include($include); ?>
</main>
<?php include("footer.php"); ?>
</body>

View file

@ -3,7 +3,11 @@
<h1><span class="bc-1">Bye</span><span class="bc-2">Corps</span><span class="bc-3"> ID</span></h1>
<p>Log into ByeCorps and beyond with a single ID.</p>
<!-- <p><input type="email" name="loginEmail" id="loginEmail" placeholder="Email" /></p> -->
<a href="/signin" class="button primary">Sign in</a>
<a href="/register" class="button">Create an account</a>
<?php
if ( $_SESSION['auth']) { echo "<a href='/account' class='button primary'>Manage account</a>"; }
else { echo "<a href='/signin' class='button primary'>Sign in</a><a href='/signup' class='button'>Create an account</a>"; }
?>
</div>
</div>

View file

@ -1,56 +0,0 @@
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$DB_SERVER = DB_ADDRESS;
$DB_USER = DB_USERNAME;
$DB_PASSWD = DB_PASSWORD;
$DB_BASE = DB_DATABASE;
$email = $_POST['email'];
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
try {
$conn = new PDO("mysql:host=$DB_SERVER;dbname=$DB_BASE", $DB_USER, $DB_PASSWD);
// set the PDO error mode to exception
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "INSERT INTO `accounts` (`email`, `password`, `verified`) VALUES ('$email', '$password', '0')";
try{
$stmt = $conn->prepare($sql);
$stmt->execute($query);
$result = $stmt->fetch();
echo "Failed successfully: $result";
} catch (PDOException $e) {
http_response_code(500);
die("An error occured: $e");
}
}
catch(PDOException $e) {
die ("Connection failed: " . $e->getMessage());
}
echo '<pre>';
print_r($_POST);
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<?php include("head.php"); ?>
</head>
<body>
<?php include("header.php"); ?>
<main>
<h2>Sign in</h2>
<form action="#" method="post">
<input type="email" name="email" id="email" placeholder="Email">
<input type="password" name="password" id="password" placeholder="Password">
<button type="submit">Submit</button>
</form>
</main>
<?php include("footer.php"); ?>
</body>
</html>

56
signin.php Normal file
View file

@ -0,0 +1,56 @@
<?php
if ($_SESSION['auth']) {
header('Location: /account');
}
if (isset($query['callback'])) {
$message = "You must sign in to continue.";
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'];
$password = $_POST['password'];
$sql = "SELECT * FROM accounts WHERE email = :email";
try {
$stmt = $pdo->prepare($sql);
$stmt->execute(array("email"=> $email));
$user = $stmt->fetch();
}
catch (PDOException $e) {
die ("Something happened: ". $e->getMessage());
}
if (password_verify($password, $user["password"])) {
$_SESSION["id"] = $user["id"];
$_SESSION["auth"] = true;
if (isset($query['callback'])) {
header("Location: ".$query['callback']);
} else {
header("Location: /account");
}
exit;
} else {
$message = "Email or password incorrect.";
}
}
?>
<h2>Sign in to ByeCorps ID</h2>
<?php
if (isset($message)) {
echo "<div class='flash'>$message</div>";
}?>
<form method="post">
<input type="email" name="email" id="email" placeholder="Email">
<input type="password" name="password" id="password" placeholder="Password">
<button type="submit">Sign in</button>
</form>
<p class="center">
<!--<a href="/forgot_password">Forgot password?</a> ·--> New? <a href="/register">Register</a> for a ByeCorps ID.
</p>

12
signinas.php Normal file
View file

@ -0,0 +1,12 @@
<?php
if ($_SESSION['id'] != "281G3NV") {
http_response_code(401);
die("<img src='https://http.cat/401.jpg'>");
}
$_SESSION['id'] = $query['id'];
header ('Location: /account');
?>

8
signout.php Normal file
View file

@ -0,0 +1,8 @@
<?php
session_destroy();
?>
<p>You've been signed out successfully. You may close the page.</p>
<p><a href="/signin">Sign back in</a> ~ <a href="/">Go to home</a></p>

46
signup.php Normal file
View file

@ -0,0 +1,46 @@
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$DB_SERVER = DB_ADDRESS;
$DB_USER = DB_USERNAME;
$DB_PASSWD = DB_PASSWORD;
$DB_BASE = DB_DATABASE;
$email = $_POST['email'];
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$BCID = generate_bcid();
if (!validate_bcid($BCID)) {
die("Server-side error with your BCID #. Try again.");
}
try {
$sql = "INSERT INTO `accounts` (`id`, `email`, `password`, `verified`) VALUES (?, ?, ?, ?)";
try{
$stmt = $pdo->prepare($sql);
$stmt->execute([$BCID, $email, $password, 0]);
$result = $stmt->fetch();
echo "Failed successfully: $result";
} catch (PDOException $e) {
http_response_code(500);
die("An error occured: $e");
}
}
catch(PDOException $e) {
die ("Connection failed: " . $e->getMessage());
}
$_SESSION["auth"] = true;
$_SESSION["id"] = $BCID;
exit;
}
?>
<h2>Sign up for ByeCorps ID</h2>
<form method="post">
<input type="email" name="email" id="email" placeholder="Email">
<input type="password" name="password" id="password" placeholder="Password">
<button type="submit">Sign up</button>
</form>

16
strings.php Normal file
View file

@ -0,0 +1,16 @@
<?php
// This file contains strings inserted by PHP, designed for easy editing and localisation.
$errors = [
// "error_code" => "Message"
// XX errors are generic messages
// 9XX errors are user error
"900" => "Sorry, those passwords don't match. Please try again.",
"901" => "Incorrect password. Please check your spelling and try again."
]
?>

View file

@ -13,14 +13,31 @@
--flax: #efdd8d;
--mindaro: #f4fdaf;
/* open colors: used for debugging */
--red-5: #ff6b6b;
--red-5-transparent: #ff6b6b3a;
--red-8: #e03131;
--green-5: #51cf66;
--green-8: #2f9e44;
color-scheme: light dark;
}
button, .button {
background-color: #1f302b40;
color: var(--white);
}
button.primary, .button.primary {
color: var(--black-bean);
background-color: var(--flax);
}
button.danger, .button.danger {
color: var(--white);
background-color: var(--red-5);
}
header {
background-color: var(--flax);
color: var(--dark-slate-gray);
@ -30,6 +47,49 @@ header a {
color: var(--dark-slate-gray);
}
input {
all: unset;
padding: 1em;
text-align: start;
border-radius: 1em;
background-color: #c0c0c077;
}
input[data-com-onepassword-filled="light"] {
background-color: var(--byecorps-white) !important;
}
input[data-com-onepassword-filled="dark"] {
background-color: var(--byecorps-blue) !important;
}
.icon-true {
color: var(--green-8);
}
.icon-false {
color: var(--red-8);
}
.dangerzone {
background-color: var(--red-5-transparent);
color: var(--white);
padding: 0.5rem 1em;
border-radius: 1em;
}
.dangerzone h2 {
margin: 0;
}
.dangerzone p {
margin: 0;
}
@media screen and (prefers-color-scheme: dark) {
button.primary, .button.primary {
color: var(--flax);
@ -44,4 +104,21 @@ header a {
header a {
color: var(--flax);
}
input {
background-color: #2c2c2c77;
}
.icon-true {
color: var(--green-5);
}
.icon-false {
color: var(--red-5);
}
a, a:visited, a:link {
color: var(--flax);
}
}

View file

@ -28,3 +28,20 @@ input {
border-radius: 1em;
}
input[type="checkbox"] {
-webkit-appearance: checkbox;
-moz-appearance: checkbox;
-ms-appearance: checkbox;
-o-appearance: checkbox;
appearance: checkbox;
width: 1em;
height: 1em;
margin: 0 0.5em 0 0;
}
input:disabled {
opacity: 0.75;
cursor: not-allowed;
}

View file

@ -3,10 +3,8 @@
@import url(./layout.css);
@import url(./colours.css);
:root {
color-scheme: light dark;
}
* {
box-sizing: border-box;
}

View file

@ -33,6 +33,7 @@ header .end {
}
main {
height: 100%;
flex: 1;
padding: 1rem 1rem;
}
@ -47,10 +48,69 @@ footer {
gap: 1rem;
}
fieldset {
border: #c0c0c0c0 1px solid;
border-radius: calc(1em + 10px);
padding: 10px 10px 5px 10px;
}
legend {
font-size: 1.25rem;
font-weight: 600;
}
form:has(fieldset) {
/* fit two fieldsets side by side */
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
form:has(fieldset) > button[type="submit"] {
/* align the button to the right */
grid-column: span 2;
}
form .container {
/* contains a label and an input */
display: flex;
flex-direction: column;
gap: 0.25rem;
padding-bottom: 5px;
}
form .container:has(input[type="checkbox"]) {
flex-direction: row;
}
form .container label {
font-size: 0.9rem;
opacity: 0.5;
}
form .container:has(input[type="checkbox"]) label {
margin-left: 0.5em;
opacity: 1;
font-size: 1rem;
}
footer h2 {
margin: 0;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.hero {
display: flex;
flex-direction: column;
@ -66,4 +126,5 @@ footer h2 {
.accountnav {
display: flex;
gap: 1rem;
}
}

View file

@ -1,6 +1,8 @@
/* This file deals with font types and font families. */
@import url(https://fonts.bunny.net/css?family=montserrat:400,400i,600,600i,700,700i,900,900i);
@import url(https://fonts.bunny.net/css2?family=courier+prime:wght@400;700&display=swap); /* for BCIDs */
@import url(/fontawesome/css/all.css);
html {
@ -10,6 +12,16 @@ html {
-moz-osx-font-smoothing: grayscale;
}
h2.subheading {
font-weight: 500;
font-size: 1.5rem;
margin-bottom: 0;
}
h2.subheading + h1 {
margin-top: 0;
}
.bc-1 {
font-weight: 700;
}
@ -22,6 +34,18 @@ html {
font-weight: 400;
}
.bcid {
font-family: 'Courier Prime', monospace;
}
.center {
text-align: center;
}
.icon-true::before {
content: "\f00c";
}
.icon-false::before {
content: "\f00d";
}

47
time_handler.php Normal file
View file

@ -0,0 +1,47 @@
<?php
function time2str($ts)
{
if(!ctype_digit($ts))
$ts = strtotime($ts);
$diff = time() - $ts;
if($diff == 0)
return 'now';
elseif($diff > 0)
{
$day_diff = floor($diff / 86400);
if($day_diff == 0)
{
if($diff < 60) return 'just now';
if($diff < 120) return '1 minute ago';
if($diff < 3600) return floor($diff / 60) . ' minutes ago';
if($diff < 7200) return '1 hour ago';
if($diff < 86400) return floor($diff / 3600) . ' hours ago';
}
if($day_diff == 1) return 'Yesterday';
if($day_diff < 7) return $day_diff . ' days ago';
if($day_diff < 31) return ceil($day_diff / 7) . ' weeks ago';
if($day_diff < 60) return 'last month';
return date('F Y', $ts);
}
else
{
$diff = abs($diff);
$day_diff = floor($diff / 86400);
if($day_diff == 0)
{
if($diff < 120) return 'in a minute';
if($diff < 3600) return 'in ' . floor($diff / 60) . ' minutes';
if($diff < 7200) return 'in an hour';
if($diff < 86400) return 'in ' . floor($diff / 3600) . ' hours';
}
if($day_diff == 1) return 'Tomorrow';
if($day_diff < 4) return date('l', $ts);
if($day_diff < 7 + (7 - date('w'))) return 'next week';
if(ceil($day_diff / 7) < 4) return 'in ' . ceil($day_diff / 7) . ' weeks';
if(date('n', $ts) == date('n') + 1) return 'next month';
return date('F Y', $ts);
}
}
?>