140 lines
3.4 KiB
PHP
140 lines
3.4 KiB
PHP
<?php
|
|||
|
|
/**
|
||
|
|
* 全局辅助函数(由 bootstrap.php 引入,页面无需重复 require)
|
||
|
|
*/
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
/** HTML 转义:所有输出到页面的动态值都必须过一道 */
|
||
|
|
function e($value): string
|
||
|
|
{
|
||
|
|
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 静态资源地址,带版本号(mtime + size)——没版本号的话浏览器会一直用旧缓存 */
|
||
|
|
function asset(string $rel): string
|
||
|
|
{
|
||
|
|
$p = dirname(__DIR__) . $rel;
|
||
|
|
if (!is_file($p)) {
|
||
|
|
return $rel;
|
||
|
|
}
|
||
|
|
return $rel . '?v=' . substr(md5(filemtime($p) . filesize($p)), 0, 8);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 站内链接(统一走 query-string 路由,不依赖 Apache 重写) */
|
||
|
|
function url(string $route = 'home', array $query = []): string
|
||
|
|
{
|
||
|
|
$q = array_merge(['r' => $route], $query);
|
||
|
|
return 'index.php?' . http_build_query($q);
|
||
|
|
}
|
||
|
|
|
||
|
|
function redirect(string $route = 'home', array $query = []): void
|
||
|
|
{
|
||
|
|
header('Location: ' . url($route, $query));
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
function is_post(): bool
|
||
|
|
{
|
||
|
|
return ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
|
||
|
|
}
|
||
|
|
|
||
|
|
function fb_config(string $key = null, $default = null)
|
||
|
|
{
|
||
|
|
$c = $GLOBALS['config'];
|
||
|
|
if ($key === null) {
|
||
|
|
return $c;
|
||
|
|
}
|
||
|
|
return array_key_exists($key, $c) ? $c[$key] : $default;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------- CSRF ----------------
|
||
|
|
function csrf_token(): string
|
||
|
|
{
|
||
|
|
if (empty($_SESSION['_csrf'])) {
|
||
|
|
$_SESSION['_csrf'] = bin2hex(random_bytes(16));
|
||
|
|
}
|
||
|
|
return $_SESSION['_csrf'];
|
||
|
|
}
|
||
|
|
|
||
|
|
function csrf_field(): string
|
||
|
|
{
|
||
|
|
return '<input type="hidden" name="_csrf" value="' . e(csrf_token()) . '">';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 校验 CSRF。失败直接结束请求 —— 不做这种假返回:
|
||
|
|
* 「返回 false 让调用层自己处理」的写法,迟早有人忘了判断。
|
||
|
|
*/
|
||
|
|
function csrf_check(): void
|
||
|
|
{
|
||
|
|
$ok = isset($_POST['_csrf'], $_SESSION['_csrf'])
|
||
|
|
&& hash_equals($_SESSION['_csrf'], (string)$_POST['_csrf']);
|
||
|
|
if (!$ok) {
|
||
|
|
http_response_code(419);
|
||
|
|
echo '页面已过期,请刷新后重试(CSRF 校验失败)';
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------- 一次性提示 ----------------
|
||
|
|
function flash(string $type, string $message): void
|
||
|
|
{
|
||
|
|
$_SESSION['_flash'][] = ['type' => $type, 'message' => $message];
|
||
|
|
}
|
||
|
|
|
||
|
|
function take_flash(): array
|
||
|
|
{
|
||
|
|
$f = $_SESSION['_flash'] ?? [];
|
||
|
|
unset($_SESSION['_flash']);
|
||
|
|
return $f;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 表单回填(校验失败时保留用户输入) */
|
||
|
|
function old(string $key, $default = '')
|
||
|
|
{
|
||
|
|
return $_SESSION['_old'][$key] ?? $default;
|
||
|
|
}
|
||
|
|
|
||
|
|
function set_old(array $data): void
|
||
|
|
{
|
||
|
|
$_SESSION['_old'] = $data;
|
||
|
|
}
|
||
|
|
|
||
|
|
function clear_old(): void
|
||
|
|
{
|
||
|
|
unset($_SESSION['_old']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 生日相关:返回年龄与距下次生日天数 */
|
||
|
|
function age_from(string $birthday): ?int
|
||
|
|
{
|
||
|
|
if ($birthday === '' || $birthday === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
$b = new DateTimeImmutable($birthday);
|
||
|
|
} catch (Exception $ex) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return (int)$b->diff(new DateTimeImmutable('today'))->y;
|
||
|
|
}
|
||
|
|
|
||
|
|
function days_until_birthday(string $birthday): ?int
|
||
|
|
{
|
||
|
|
if (!$birthday) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
$b = new DateTimeImmutable($birthday);
|
||
|
|
$now = new DateTimeImmutable('today');
|
||
|
|
$next = new DateTimeImmutable($now->format('Y') . '-' . $b->format('m-d'));
|
||
|
|
if ($next < $now) {
|
||
|
|
$next = $next->modify('+1 year');
|
||
|
|
}
|
||
|
|
return (int)$now->diff($next)->days;
|
||
|
|
} catch (Exception $ex) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|