Browse Source

v1.1.4

pull/226/head
xiaoz 1 day ago
parent
commit
4805a6fad1
  1. 2
      README.md
  2. 207
      class/Api.php
  3. 4
      controller/api.php
  4. 32
      controller/index.php
  5. 6
      data/update.log
  6. 2
      templates/admin/add_link.php
  7. 2
      templates/admin/header.php
  8. 80
      templates/admin/setting/theme.php
  9. 4
      templates/default2/index.php
  10. 4
      templates/default2/info.json
  11. 2
      version.txt

2
README.md

@ -50,7 +50,7 @@ OneNav 是一款功能强大且简洁高效的浏览器书签管理器,支持 @@ -50,7 +50,7 @@ OneNav 是一款功能强大且简洁高效的浏览器书签管理器,支持
**底部工具栏**
底部工具栏默认对访客隐藏,只有管理员登录后才会显示,支持5个操作按钮,分别是:添加链接、返回顶部、订阅管理、系统状态、后台管理。
底部工具栏默认对访客隐藏,只有管理员登录后才会显示,支持5个操作按钮,分别是:添加链接、返回顶部、订阅管理、系统状态、后台管理。
![12efff04c347d853.png](https://img.rss.ink/imgs/2024/11/28/12efff04c347d853.png)

207
class/Api.php

@ -1252,40 +1252,166 @@ class Api { @@ -1252,40 +1252,166 @@ class Api {
/**
* 获取链接信息
*/
public function get_link_info($token,$url){
/**
* 获取链接的标题和描述信息 (优化版)
*
* @param string $token 用于认证的令牌
* @param string $url 需要获取信息的URL
* @return void 直接输出JSON格式的返回结果
*/
public function get_link_info($token, $url)
{
$this->auth($token);
//检查链接是否合法
$pattern = "/^(http:\/\/|https:\/\/).*/";
//链接不合法
if( empty($url) ) {
$this->err_msg(-2000,'URL不能为空!');
}
if( !preg_match($pattern,$url) ){
$this->err_msg(-1010,'只支持识别http/https协议的链接!');
}
else if( !filter_var($url, FILTER_VALIDATE_URL) ) {
$this->err_msg(-2000,'只支持识别http/https协议的链接!');
}
//获取网站标题
$c = curl_init();
curl_setopt($c, CURLOPT_URL, $url);
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($c, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($c, CURLOPT_SSL_VERIFYHOST, false);
//设置超时时间
curl_setopt($c , CURLOPT_TIMEOUT, 10);
$data = curl_exec($c);
curl_close($c);
$pos = strpos($data,'utf-8');
if($pos===false){$data = iconv("gbk","utf-8",$data);}
preg_match("/<title>(.*)<\/title>/i",$data, $title);
$link['title'] = $title[1];
//获取网站描述
$tags = get_meta_tags($url);
$link['description'] = $tags['description'];
// 步骤 1: 验证URL的有效性
if (empty($url)) {
$this->err_msg(-2000, 'URL不能为空!');
}
// 使用 filter_var 和正则表达式进行双重验证,确保是有效的http/https链接
if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match("/^https?:\/\//i", $url)) {
$this->err_msg(-1010, '只支持识别http/https协议的链接!');
}
// 初始化返回数据结构,确保即使获取失败也有完整的字段
$link = [
'title' => '',
'description' => ''
];
// 步骤 2: 使用cURL获取网页内容(已优化)
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
// 返回内容但不直接输出
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// 获取HTTP头信息,用于编码检测
curl_setopt($ch, CURLOPT_HEADER, 1);
// 自动跟随重定向
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// 最大重定向次数
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
// 连接超时时间(秒)
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
// 总执行超时时间(秒)
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
// 设置浏览器User Agent,防止被部分网站屏蔽
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36');
// 兼容https,原逻辑保留,但注意在生产环境中这可能带来安全风险
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// 性能优化:只下载前500KB内容,通常足以获取head信息
curl_setopt($ch, CURLOPT_RANGE, '0-512000');
$response = curl_exec($ch);
// 增加cURL错误处理
if (curl_errno($ch)) {
$this->err_msg(-2001, '链接无法访问或超时: ' . curl_error($ch));
}
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// 增加HTTP状态码判断
if ($http_code < 200 || $http_code >= 400) {
$this->err_msg(-2002, '链接返回无效的HTTP状态码: ' . $http_code);
}
// 分离响应头和响应体
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header_str = substr($response, 0, $header_size);
$body = substr($response, $header_size);
curl_close($ch);
// 如果响应体为空,直接返回空信息
if (empty($body)) {
exit(json_encode(['code' => 0, 'data' => $link]));
}
// 步骤 3: 智能检测编码并转换为UTF-8
$encoding = '';
// 方法A: 从HTTP响应头中提取编码
if (preg_match('/charset=([\w-]+)/i', $header_str, $matches)) {
$encoding = strtoupper($matches[1]);
}
// 方法B: 如果响应头没有,则从HTML的meta标签中提取
if (empty($encoding) && preg_match('/<meta.*?charset=["\']?([\w-]+)["\']?/i', $body, $matches)) {
$encoding = strtoupper($matches[1]);
}
// 方法C: 如果以上都没有,则自动检测
if (empty($encoding)) {
$encoding = mb_detect_encoding($body, ['UTF-8', 'GBK', 'GB2312', 'BIG5', 'ISO-8859-1'], true);
}
// 如果检测到编码且不是UTF-8,则进行转换
if ($encoding && $encoding !== 'UTF-8') {
$body = @mb_convert_encoding($body, 'UTF-8', $encoding);
}
// 步骤 4: 使用DOMDocument解析HTML,更稳定可靠
$doc = new DOMDocument();
// 抑制因HTML不规范而产生的警告
libxml_use_internal_errors(true);
// 明确告知DOMDocument以UTF-8编码加载,防止二次乱码
$doc->loadHTML('<?xml encoding="UTF-8">' . $body);
// 清理错误缓存
libxml_clear_errors();
$xpath = new DOMXPath($doc);
// 步骤 5: 提取标题(带后备方案)
// 方案1: 获取 <title> 标签
$title_node = $xpath->query('//title')->item(0);
if ($title_node) {
$link['title'] = trim($title_node->nodeValue);
}
// 方案2: 获取 og:title
if (empty($link['title'])) {
$og_title_node = $xpath->query('//meta[@property="og:title"]/@content')->item(0);
if ($og_title_node) {
$link['title'] = trim($og_title_node->nodeValue);
}
}
// 方案3: 获取第一个 <h1> 标签
if (empty($link['title'])) {
$h1_node = $xpath->query('//h1')->item(0);
if ($h1_node) {
$link['title'] = trim($h1_node->nodeValue);
}
}
// 步骤 6: 提取描述(带后备方案)
// 方案1: 获取 <meta name="description">
$desc_node = $xpath->query('//meta[@name="description"]/@content')->item(0);
if ($desc_node) {
$link['description'] = trim($desc_node->nodeValue);
}
// 方案2: 获取 og:description
if (empty($link['description'])) {
$og_desc_node = $xpath->query('//meta[@property="og:description"]/@content')->item(0);
if ($og_desc_node) {
$link['description'] = trim($og_desc_node->nodeValue);
}
}
// 方案3: 获取第一个有意义的 <p> 标签内容
if (empty($link['description'])) {
$p_nodes = $xpath->query('//p');
foreach ($p_nodes as $p_node) {
$p_text = trim($p_node->nodeValue);
// 选取一个长度较长的段落作为描述
if (mb_strlen($p_text, 'UTF-8') > 30) {
$link['description'] = $p_text;
break;
}
}
}
// 对获取到的结果进行HTML实体解码,使显示更友好
if($link['title']) {
$link['title'] = html_entity_decode($link['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
if($link['description']) {
$link['description'] = html_entity_decode($link['description'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// 步骤 7: 按指定格式返回成功结果
$data = [
'code' => 0,
'data' => $link
@ -1611,6 +1737,23 @@ class Api { @@ -1611,6 +1737,23 @@ class Api {
$this->return_json(200,$theme_config,"");
}
// 获取设置的主题
public function get_themes() {
$theme = $this->db->get("on_options","value",[
"key" => "s_themes"
]);
// 字符串转换为数组
$theme = json_decode($theme, true);
//如果主题不存在,则返回默认主题
if( empty($theme) ) {
$theme = [
"pc_theme" => "default2",
"mobile_theme" => "default2"
];
}
$this->return_json(200,$theme,"");
}
/**
* 通用json消息返回
*/

4
controller/api.php

@ -324,8 +324,8 @@ function exe_sql($api) { @@ -324,8 +324,8 @@ function exe_sql($api) {
//设置options表
function set_theme($api) {
$key = 'theme';
$value = htmlspecialchars($_POST['value']);
$key = 's_themes';
$value = $_POST['value'];
$api->set_option($key,$value);
}

32
controller/index.php

@ -204,10 +204,36 @@ else{ @@ -204,10 +204,36 @@ else{
// 载入前台首页模板
//查询主题设置
$template = $db->get("on_options","value",[
"key" => "theme"
//查询用户设置的主题,区分PC和手机
$templates = $db->get("on_options","value",[
"key" => "s_themes"
]);
// 获取UA
$userAgent = $_SERVER['HTTP_USER_AGENT'];
$mobileKeywords = [
'mobile', 'android', 'iphone'
];
// 如果查询结果是空的
if (empty($templates)) {
// 设置默认主题
$template = 'default2';
} else {
// 不为空,则根据情况使用不同主题
foreach ($mobileKeywords as $keyword) {
if (stripos($userAgent, $keyword) !== false) {
// 设置主题
$template = json_decode($templates)->mobile_theme;
break;
}
else{
// 设置主题
$template = json_decode($templates)->pc_theme;
}
}
}
//获取用户传递的主题参数
$theme = trim( @$_GET['theme'] );
//如果用户传递了主题参数

6
data/update.log

@ -1,3 +1,9 @@ @@ -1,3 +1,9 @@
2025.07.07
1. 修复说明文件中的错别字
2. 去掉顶部的技术支持
3. 进一步优化了链接识别功能,提高成功率
4. 可以单独设置PC和手机主题
2025.05.15
1. 【后台 - 分类列表】,添加了添加分类按钮
2. 删除分类时,如果下面存在子分类,则不允许删除

2
templates/admin/add_link.php

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
<div class="layui-col-lg12">
<div class="setting-msg">
<p>1. 权重越大,排序越靠前</p>
<p>2. 识别功能可以自动获取链接标题和描述信息,不确保一定成功</p>
<p>2. 识别功能可以自动获取链接标题和描述信息,该功能处于测试阶段,不确保一定成功</p>
<p>3. 仅 default2/5iux/heimdall/tushan2/webstack 支持自定义图标,其余主题均自动获取链接图标。</p>
</div>
</div>

2
templates/admin/header.php

@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
<li class="layui-nav-item"><a href="/index.php?c=admin&page=link_list"><i class="layui-icon layui-icon-link"></i> 我的链接</a></li>
<li class="layui-nav-item"><a href="/index.php?c=admin&page=add_link"><i class="layui-icon layui-icon-add-circle-fine"></i> 添加链接</a></li> -->
<li class="layui-nav-item"><a title = "加入OneNav交流群" target = "_blank" href="https://dwz.ovh/qxsul"><i class="layui-icon layui-icon-group"></i> 交流群</a></li>
<li class="layui-nav-item"><a onclick = "support()" title = "请求OneNav技术支持" href="javascript:;"><i class="layui-icon layui-icon-help"></i> 技术支持</a></li>
<!-- <li class="layui-nav-item"><a onclick = "support()" title = "请求OneNav技术支持" href="javascript:;"><i class="layui-icon layui-icon-help"></i> 技术支持</a></li> -->
</ul>
<ul class="layui-nav layui-layout-right">

80
templates/admin/setting/theme.php

@ -17,6 +17,39 @@ @@ -17,6 +17,39 @@
<div class="layui-col-lg12">
<div class="layui-row layui-col-space24">
<!-- 主题设置 -->
<div class="setting">
<form class="layui-form layui-form-pane" lay-filter="themes" action="">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">PC主题</label>
<div class="layui-input-inline">
<select name="pc_theme" lay-filter="aihao">
<?php foreach ($themes as $key => $theme) { ?>
<option value="<?php echo $key; ?>"><?php echo $key; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">手机主题</label>
<div class="layui-input-inline">
<select name="mobile_theme" lay-filter="aihao">
<?php foreach ($themes as $key => $theme) { ?>
<option value="<?php echo $key; ?>"><?php echo $key; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="s_themes">保存</button>
</div>
</div>
</form>
</div>
<!-- 主题设置选项 -->
<h2>已下载</h2>
<!-- 主题列表new -->
<?php foreach ($themes as $key => $theme) {
@ -43,16 +76,14 @@ @@ -43,16 +76,14 @@
<!-- 主题图片 -->
<div class = "screenshot">
<p><img layer-src="<?php echo $theme['info']->screenshot; ?>" src="<?php echo $theme['info']->screenshot; ?>" alt=""></p>
<?php if( $current_them == $key ) { ?>
<div class="in-use">使用中</div>
<?php } ?>
</div>
<!-- 主题图片END -->
<hr>
<div class = "thme-btns">
<div class="layui-btn-group">
<button type="button" class="layui-btn layui-btn-sm" onclick = "set_theme('<?php echo $key; ?>')">使用</button>
<!-- <button type="button" class="layui-btn layui-btn-sm" onclick = "set_theme('<?php echo $key; ?>')">使用</button> -->
<button type="button" class="layui-btn layui-btn-sm" onclick = "theme_detail('<?php echo $key; ?>')">详情</button>
<button type="button" class="layui-btn layui-btn-sm" onclick = "theme_config('<?php echo $key; ?>')">参数设置</button>
<button type="button" class="layui-btn layui-btn-sm" onclick = "update_theme('<?php echo $key; ?>','<?php echo $theme['info']->version; ?>')">更新</button>
@ -72,7 +103,7 @@ @@ -72,7 +103,7 @@
<hr>
<!-- 在线主题 -->
<div class="layui-col-lg12">
<h2 style = "padding-bottom:16px;padding-top:8px;">在线主题</h2>
<h2 style = "padding-bottom:16px;padding-top:8px;">在线主题</h2>
<div class="layui-row layui-col-space24">
<?php foreach ($theme_list as $key => $theme) {
//var_dump($theme['info']->name);
@ -110,8 +141,43 @@ @@ -110,8 +141,43 @@
</div>
<?php include_once(dirname(__DIR__).'/footer.php'); ?>
<script>
layui.use('layer', function(){
layui.use(['layer','form'], function(){
var layer = layui.layer;
var form = layui.form;
// 监听主题设置的提交
form.on('submit(s_themes)', function(data){
//console.log(data.field);
let value = JSON.stringify(data.field);
$.post("/index.php?c=api&method=set_theme",{value:value},function(res){
if( res.code == 0 ) {
layer.msg(res.data, {icon: 1});
// setTimeout(() => {
// location.reload();
// }, 2000);
}
else{
layer.msg(res.err_msg, {icon: 5});
}
});
return false; //阻止表单跳转。如果需要表单跳转,去掉这段即可。
});
// 请求API接口获取当前设置的主题
$.get("/index.php?c=api&method=get_themes",function(data,status){
if( data.code == 200 ) {
let value = data.data;
//console.log(themes);
//设置主题下拉框的值
form.val('themes', {
'pc_theme': value.pc_theme,
'mobile_theme': value.mobile_theme
});
}
else{
layer.msg(data.err_msg, {icon: 5});
}
});
});
function theme_detail(name){
layer.open({
@ -229,7 +295,7 @@ function update_theme(name,version){ @@ -229,7 +295,7 @@ function update_theme(name,version){
//遍历所有主题,检查是否有更新
function check_update(){
console.log('fdsfsdf');
// console.log('fdsfsdf');
//请求远程主题列表
$.get("https://onenav.xiaoz.top/v1/theme_list.php",function(data,status){
let result = data.data;

4
templates/default2/index.php

@ -8,9 +8,9 @@ @@ -8,9 +8,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>
<?php echo $site[ 'title']; ?> - <?php echo $site[ 'subtitle']; ?></title>
<script type="module" crossorigin src="/templates/<?php echo $template; ?>/assets/index.js?v=<?php echo $info->version; ?>""></script>
<script type="module" crossorigin src="/templates/<?php echo $template; ?>/assets/index.js?v=<?php echo $info->version; ?>"></script>
</script>
<link href="/templates/<?php echo $template; ?>/assets/index.css?v=<?php echo $info->version; ?>"" rel="stylesheet">
<link href="/templates/<?php echo $template; ?>/assets/index.css?v=<?php echo $info->version; ?>" rel="stylesheet">
<?php echo $site['custom_header']; ?>
<link rel="manifest" href="/templates/<?php echo $template; ?>/manifest.json" />
</head>

4
templates/default2/info.json

@ -3,8 +3,8 @@ @@ -3,8 +3,8 @@
"description": "OneNav目前功能最强大的默认主题,推荐使用。",
"homepage": "https:\/\/blog.xiaoz.org",
"help_url":"https://dwz.ovh/gnae4",
"version": "1.3.0",
"update": "2025\/05\/05",
"version": "1.3.1",
"update": "2025\/05\/25",
"author": "xiaoz<xiaoz93@outlook.com>",
"screenshot": "https://v.png.pub/imgs/2024/11/27/c01894e5d9e0d850.png",
"demo":"https://nav.rss.ink",

2
version.txt

@ -1 +1 @@ @@ -1 +1 @@
v1.1.3-20250515
v1.1.4-20250709
Loading…
Cancel
Save