Time Zones Are Hard - https://tz.rita.moe

Use constants and add security headers

Changed files
+69 -52
+69 -52
index.php
···
<?php
+
// Configuration
+
const COOKIE_LIFETIME = 60 * 60 * 24 * 365; // 1 year
+
const SITE_URL = 'https://tz.rita.moe';
+
+
// Set security headers
+
$nonce = bin2hex(random_bytes(16));
+
header("Content-Security-Policy: default-src 'none'; script-src 'self' 'nonce-$nonce'; img-src 'self'; style-src 'self'; form-action 'self';");
+
header('X-Content-Type-Options: nosniff');
+
header('X-Frame-Options: DENY');
+
header('X-XSS-Protection: 1; mode=block');
+
function getOffset($tz) {
$tz = new DateTime('now', new DateTimeZone($tz));
$offset = $tz->format('T');
···
}
function setUserCookie($name, $value) {
-
setcookie($name, $value, time() + 60 * 60 * 24 * 365, '/');
+
setcookie($name, $value, time() + COOKIE_LIFETIME, '/');
$_COOKIE[$name] = $value;
}
$popTZ = [
-
'America/Los_Angeles',
-
'America/New_York',
-
'Europe/London',
-
'Europe/Paris',
-
'Asia/Dubai',
-
'Asia/Jakarta',
-
'Asia/Tokyo',
-
'Australia/Sydney',
+
'America/Los_Angeles', // PST/PDT
+
'America/New_York', // EST/EDT
+
'Europe/London', // BST/GMT
+
'Europe/Paris', // CEST/CET
+
'Asia/Dubai', // UTC+04 "UAE"
+
'Asia/Jakarta', // WIB
+
'Asia/Tokyo', // JST
+
'Australia/Sydney', // AEST/AEDT
'UTC'
];
+
+
$allTZ = timezone_identifiers_list(DateTimeZone::ALL);
// User sets timezone cookie
-
if (isset($_POST['user-tz']) &&
-
in_array($_POST['user-tz'], timezone_identifiers_list(DateTimeZone::ALL))) {
+
if (isset($_POST['user-tz']) && in_array($_POST['user-tz'], $allTZ)) {
setUserCookie('user-tz', $_POST['user-tz']);
}
···
}
// Micro-routing
-
if (isset($_POST['datetime']) && isset($_POST['timezone'])) {
-
// -- Redirect to submitted date
-
$postTZ = $_POST['timezone'];
+
try {
+
if (isset($_POST['datetime']) && isset($_POST['timezone'])) {
+
// -- Redirect to submitted date
+
$postTZ = $_POST['timezone'];
-
// Handle if timezone is an offset
-
if (is_numeric($_POST['timezone'])) {
-
$isNeg = str_starts_with($postTZ, '-');
-
$h = str_pad(abs(intdiv($postTZ, 60)), 2, '0', STR_PAD_LEFT);
-
$m = str_pad($postTZ % 60, 2, '0', STR_PAD_LEFT);
-
$postTZ = ($isNeg ? '-' : '+') . $h . $m;
-
}
+
// Handle if timezone is an offset
+
if (is_numeric($_POST['timezone'])) {
+
$isNeg = str_starts_with($postTZ, '-');
+
$h = str_pad(abs(intdiv($postTZ, 60)), 2, '0', STR_PAD_LEFT);
+
$m = str_pad($postTZ % 60, 2, '0', STR_PAD_LEFT);
+
$postTZ = ($isNeg ? '-' : '+') . $h . $m;
+
}
-
// Make our date object
-
$dateObj = new DateTime($_POST['datetime'], new DateTimeZone($postTZ));
+
// Make our date object
+
$dateObj = new DateTime($_POST['datetime'], new DateTimeZone($postTZ));
-
// Redirect, with the "+" replaced by "_"
-
header('Location: /' . str_replace('+', '_', $dateObj->format('c')));
-
exit;
-
} elseif ($_SERVER['REQUEST_URI'] !== '/') {
-
// Remove leading "/"
-
$req = substr($_SERVER['REQUEST_URI'], 1);
+
// Redirect, with the "+" replaced by "_"
+
header('Location: /' . str_replace('+', '_', $dateObj->format('c')));
+
exit;
+
} elseif ($_SERVER['REQUEST_URI'] !== '/') {
+
// Remove leading "/"
+
$req = substr($_SERVER['REQUEST_URI'], 1);
-
// -- Show date infos
-
// First check if date is following the format
-
$re = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\_|-]\d{2}:\d{2}$/';
-
if (!preg_match($re, $req)) {
-
http_response_code(404);
-
$error = 'Date is not valid, wrong format, or file not found.';
-
} else {
-
// Make our date object, convert back "_" to "+"
-
$dt = new DateTime(str_replace('_', '+', $req));
+
// -- Show date infos
+
// First check if date is following the format
+
$re = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\_|-]\d{2}:\d{2}$/';
+
if (!preg_match($re, $req)) {
+
http_response_code(404);
+
$error = 'Date is not valid, wrong format, or file not found.';
+
} else {
+
// Make our date object, convert back "_" to "+"
+
$dt = new DateTime(str_replace('_', '+', $req));
+
}
}
+
} catch (Exception $e) {
+
http_response_code(400);
+
$error = 'Invalid date format or date out of range.';
}
?>
<!DOCTYPE html>
···
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>Time Zones Are Hard<?= isset($req) ? ' - ' . str_replace('_', '+', $req) : '' ?></title>
+
<title>Time Zones Are Hard<?= isset($req) ? ' - ' . htmlspecialchars(str_replace('_', '+', $req), ENT_QUOTES, 'UTF-8') : '' ?></title>
<link rel="stylesheet" href="/css/styles.css">
<?php if (isset($dt)) { ?>
<meta name="robots" content="noindex,noarchive">
<script src="/js/dayjs.min.js"></script>
<script src="/js/relativeTime.js"></script>
-
<script>
+
<script nonce="<?= $nonce ?>">
dayjs.extend(window.dayjs_plugin_relativeTime)
</script>
<?php } ?>
···
?>
Please select your local time zone <small>(or enable JavaScript)</small>:<br/>
<form method="post">
-
<select required id="user-tz" name="user-tz">
+
<select required id="user-tz" name="user-tz" aria-label="Timezone selector">
<option selected disabled>(pick one)</option>
<optgroup label="Popular">
<?php foreach($popTZ as $tz) { ?>
···
<?php } ?>
</optgroup>
<optgroup label="All">
-
<?php foreach(timezone_identifiers_list(DateTimeZone::ALL) as $tz) { ?>
+
<?php foreach($allTZ as $tz) { ?>
<option value="<?= $tz ?>">
<?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
</option>
···
</optgroup>
</select>
<br/>
-
<input type="checkbox" id="user-24" name="user-24"/>
+
<input type="checkbox" id="user-24" name="user-24" aria-label="Use 24-hour time format instead of 12-hour AM/PM"/>
<label for="user-24"><span>24h format</span></label>
<br/>
<button type="submit">Submit</button>
···
</div>
<div class="share">
-
<input type="hidden" id="url" value="https://tz.rita.moe<?= $_SERVER['REQUEST_URI'] ?>"/>
+
<input type="hidden" id="url" value="<?= htmlspecialchars(SITE_URL . $_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8') ?>"/>
</div>
-
<script>
+
<script nonce="<?= $nonce ?>">
// Make our day.js object
const djs = dayjs('<?= $dt->format('c') ?>')
···
if (navigator.canShare) {
// Use native navigator share
navigator.share({
-
url: 'https://tz.rita.moe<?= $_SERVER['REQUEST_URI'] ?>'
+
url: '<?= htmlspecialchars(SITE_URL . $_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8') ?>'
})
} else if (navigator.clipboard) {
// Use Clipboard API
-
navigator.clipboard.writeText('https://tz.rita.moe<?= $_SERVER['REQUEST_URI'] ?>')
+
navigator.clipboard.writeText('<?= htmlspecialchars(SITE_URL . $_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8') ?>')
.then(() => {
alert('URL copied to clipboard!')
})
···
alert('URL copied to clipboard!')
} else {
// Fallback to prompt with URL
-
prompt('Share this URL:', document.querySelector('#url').value)
+
prompt('Share this URL:', shareTextInput.value)
}
}
</script>
···
<?php } ?>
</optgroup>
<optgroup label="All">
-
<?php foreach(timezone_identifiers_list(DateTimeZone::ALL) as $tz) { ?>
+
<?php foreach($allTZ as $tz) { ?>
<option value="<?= $tz ?>">
<?= str_replace('_', ' ', $tz) ?> (<?= getOffset($tz) ?>)
</option>
···
<button type="submit">Submit</button>
</form>
-
<script>
+
<script nonce="<?= $nonce ?>">
// Get a date object
const dt = new Date()
···
</footer>
<?php if ($_COOKIE['has-js'] !== '1') { ?>
-
<script>
+
<script nonce="<?= $nonce ?>">
// Save on the document size if user has JavaScript
const date = new Date()
-
date.setTime(date.getTime() + (1000 * 60 * 60 * 24 * 365))
+
date.setTime(date.getTime() + (1000 * <?= COOKIE_LIFETIME ?>))
document.cookie = 'has-js=1; expires=' + date.toUTCString() + '; path=/'
</script>
<?php } ?>