Files
SimpleNav/functions.php
T
laoyang 6e83cc3ecc feat: 简航 SimpleNav 个人网站导航系统(PHP + SQLite)
- 前台:搜索引擎切换+联想下拉、网格板块(常用/工具)、合集标签页
- 后台:初始化/登录、网站/板块/引擎增删改、上下移排序
- 添加网站自动抓取名称与图标(存储于服务器),支持自定义简称
- 一键全部更新(批量刷新图标+名称,保留自定义简称)
- 搜索联想服务端代理(必应/百度/谷歌/搜狗/360/DuckDuckGo)
- 零依赖:原生 CSS/JS,PDO SQLite
2026-09-28 15:48:35 +08:00

238 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 简航 SimpleNav —— 公共函数
*/
declare(strict_types=1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php';
/** HTML 转义 */
function e(?string $s): string
{
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/** 规范化 URL:补协议、去空白 */
function norm_url(string $url): string
{
$url = trim($url);
if ($url === '') {
return '';
}
if (!preg_match('#^https?://#i', $url)) {
$url = 'http://' . $url;
}
return $url;
}
/** 取主域名(用于 favicon 与文件名) */
function domain_of(string $url): string
{
$host = parse_url(norm_url($url), PHP_URL_HOST);
return $host ? preg_replace('/^www\./i', '', $host) : '';
}
/** curl 抓取文本(带 UA、超时、跟随跳转、限长) */
function http_get(string $url, int $maxBytes = 300000, int $timeout = 8): ?array
{
if (!function_exists('curl_init')) {
// 兜底:allow_url_fopen
$ctx = stream_context_create(['http' => [
'timeout' => $timeout,
'header' => "User-Agent: Mozilla/5.0 SimpleNav\r\n",
'follow_location' => 1,
]]);
$body = @file_get_contents($url, false, $ctx, 0, $maxBytes);
if ($body === false) {
return null;
}
return ['body' => $body, 'url' => $url, 'type' => ''];
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; SimpleNav/1.0)',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_ENCODING => '',
]);
$body = curl_exec($ch);
$type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$eff = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
$err = curl_errno($ch);
curl_close($ch);
if ($err || $body === false) {
return null;
}
return ['body' => $body, 'url' => $eff ?: $url, 'type' => $type];
}
/**
* 抓取网站元信息:标题、名称、图标地址。
* @return array{title:string,name:string,icon:string,final_url:string}
*/
function fetch_site_meta(string $url): array
{
$url = norm_url($url);
$out = ['title' => '', 'name' => '', 'icon' => '', 'final_url' => $url];
$resp = http_get($url);
if ($resp === null) {
return $out;
}
$html = $resp['body'];
$final = $resp['url'];
$out['final_url'] = $final;
// 编码:优先按 <meta charset> 转换到 UTF-8
$html = ensure_utf8($html);
// <title>
if (preg_match('#<title[^>]*>(.*?)</title>#is', $html, $m)) {
$out['title'] = trim(preg_replace('/\s+/', ' ', html_entity_decode($m[1], ENT_QUOTES, 'UTF-8')));
}
// og:site_name 优先作为"名称"
if (preg_match('#<meta[^>]+property=["\']og:site_name["\'][^>]+content=["\']([^"\']+)#i', $html, $m)) {
$out['name'] = trim(html_entity_decode($m[1], ENT_QUOTES, 'UTF-8'));
} elseif ($out['title'] !== '') {
// 用标题的首段(按分隔符切分)作为名称
$t = preg_split('#[\|\-_—·::]#u', $out['title']);
$out['name'] = trim($t[0]);
}
// 图标:<link rel="...icon...">
$icon = '';
if (preg_match_all('#<link[^>]+>#i', $html, $links)) {
foreach ($links[0] as $tag) {
if (preg_match('#rel=["\'][^"\']*icon[^"\']*["\']#i', $tag)) {
if (preg_match('#href=["\']([^"\']+)["\']#i', $tag, $hm)) {
$cand = html_entity_decode($hm[1], ENT_QUOTES, 'UTF-8');
// 优先 apple-touch-icon(通常更大更清晰)
if (stripos($tag, 'apple-touch') !== false) {
$icon = $cand;
break;
}
if ($icon === '') {
$icon = $cand;
}
}
}
}
}
if ($icon !== '') {
$out['icon'] = absolutize($icon, $final);
} else {
$p = parse_url($final);
if (!empty($p['scheme']) && !empty($p['host'])) {
$out['icon'] = $p['scheme'] . '://' . $p['host'] . '/favicon.ico';
}
}
return $out;
}
/** 把可能为相对路径的 URL 补全为绝对 URL */
function absolutize(string $href, string $base): string
{
if (preg_match('#^https?://#i', $href)) {
return $href;
}
$p = parse_url($base);
$root = ($p['scheme'] ?? 'http') . '://' . ($p['host'] ?? '');
if (strpos($href, '//') === 0) {
return ($p['scheme'] ?? 'http') . ':' . $href;
}
if (strpos($href, '/') === 0) {
return $root . $href;
}
$dir = rtrim(dirname($p['path'] ?? '/'), '/');
return $root . $dir . '/' . $href;
}
/** 简单编码归一:识别 GBK/GB18030 并转 UTF-8 */
function ensure_utf8(string $s): string
{
if (preg_match('#<meta[^>]+charset=["\']?\s*(gbk|gb2312|gb18030)#i', $s)) {
if (function_exists('mb_convert_encoding')) {
return mb_convert_encoding($s, 'UTF-8', 'GB18030');
}
}
if (function_exists('mb_check_encoding') && !mb_check_encoding($s, 'UTF-8')) {
return mb_convert_encoding($s, 'UTF-8', 'GB18030');
}
return $s;
}
/**
* 下载图标并保存到本地(PNG,64x64,透明背景)。
* @return string 本地文件名(如 ab12cd.png),失败返回 ''
*/
function download_icon(string $iconUrl, string $key, int $size = 64): string
{
if ($iconUrl === '') {
return '';
}
if (!is_dir(ICON_PATH)) {
@mkdir(ICON_PATH, 0755, true);
}
$resp = http_get($iconUrl, 500000, 8);
if ($resp === null || $resp['body'] === '') {
return '';
}
$bin = $resp['body'];
$name = md5($key) . '.png';
$dest = ICON_PATH . '/' . $name;
// 用 GD 统一转成 PNG(顺带裁成正方形、缩放)
if (function_exists('imagecreatefromstring')) {
$im = @imagecreatefromstring($bin);
if ($im !== false) {
$w = imagesx($im);
$h = imagesy($im);
$side = min($w, $h);
$square = imagecreatetruecolor($size, $size);
imagealphablending($square, false);
imagesavealpha($square, true);
$trans = imagecolorallocatealpha($square, 0, 0, 0, 127);
imagefill($square, 0, 0, $trans);
$sx = (int)(($w - $side) / 2);
$sy = (int)(($h - $side) / 2);
imagecopyresampled($square, $im, 0, 0, $sx, $sy, $size, $size, $side, $side);
imagepng($square, $dest);
imagedestroy($square);
imagedestroy($im);
return $name;
}
}
// 无 GD:原样保存(按 content-type 猜扩展名)
$ext = 'ico';
if (stripos($resp['type'], 'png') !== false) {
$ext = 'png';
} elseif (stripos($resp['type'], 'svg') !== false) {
$ext = 'svg';
} elseif (stripos($resp['type'], 'jpeg') !== false || stripos($resp['type'], 'jpg') !== false) {
$ext = 'jpg';
} elseif (stripos($resp['type'], 'gif') !== false) {
$ext = 'gif';
}
$name = md5($key) . '.' . $ext;
@file_put_contents(ICON_PATH . '/' . $name, $bin);
return is_file(ICON_PATH . '/' . $name) ? $name : '';
}
/** 图标显示地址(为空时返回 '',前端回退到首字母块) */
function icon_src(string $file): string
{
return $file === '' ? '' : url(ICON_URL . '/' . $file);
}
/** 名称首字配色(无图标时的回退头像底色) */
function letter_color(string $s): string
{
$palette = ['#2F6BF7', '#5B8DEF', '#0EA5E9', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316'];
return $palette[abs(crc32($s)) % count($palette)];
}