From 4805a6fad1596d40de21967fb560ea889c5dc828 Mon Sep 17 00:00:00 2001 From: xiaoz Date: Wed, 9 Jul 2025 08:42:38 +0800 Subject: [PATCH] v1.1.4 --- README.md | 2 +- class/Api.php | 211 +++++++++++++++++++++++++----- controller/api.php | 4 +- controller/index.php | 32 ++++- data/update.log | 6 + templates/admin/add_link.php | 2 +- templates/admin/header.php | 2 +- templates/admin/setting/theme.php | 82 ++++++++++-- templates/default2/index.php | 4 +- templates/default2/info.json | 4 +- version.txt | 2 +- 11 files changed, 296 insertions(+), 55 deletions(-) mode change 100755 => 100644 templates/default2/info.json diff --git a/README.md b/README.md index 690a41e..739ef8a 100755 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ OneNav 是一款功能强大且简洁高效的浏览器书签管理器,支持 **底部工具栏** -底部工具栏默认对访客隐藏,只有党管理员登录后才会显示,支持5个操作按钮,分别是:添加链接、返回顶部、订阅管理、系统状态、后台管理。 +底部工具栏默认对访客隐藏,只有当管理员登录后才会显示,支持5个操作按钮,分别是:添加链接、返回顶部、订阅管理、系统状态、后台管理。 ![12efff04c347d853.png](https://img.rss.ink/imgs/2024/11/28/12efff04c347d853.png) diff --git a/class/Api.php b/class/Api.php index 9aef3e1..3505ea5 100755 --- a/class/Api.php +++ b/class/Api.php @@ -1250,45 +1250,171 @@ 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>/i",$data, $title); + + // 步骤 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'); - $link['title'] = $title[1]; + $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); + } - //获取网站描述 - $tags = get_meta_tags($url); - $link['description'] = $tags['description']; + // 分离响应头和响应体 + $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 + 'code' => 0, + 'data' => $link ]; exit(json_encode($data)); } @@ -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消息返回 */ diff --git a/controller/api.php b/controller/api.php index dd89994..d8193e7 100755 --- a/controller/api.php +++ b/controller/api.php @@ -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); } diff --git a/controller/index.php b/controller/index.php index e6f97ba..c6793c2 100755 --- a/controller/index.php +++ b/controller/index.php @@ -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'] ); //如果用户传递了主题参数 diff --git a/data/update.log b/data/update.log index 002baf6..c347bab 100755 --- a/data/update.log +++ b/data/update.log @@ -1,3 +1,9 @@ +2025.07.07 +1. 修复说明文件中的错别字 +2. 去掉顶部的技术支持 +3. 进一步优化了链接识别功能,提高成功率 +4. 可以单独设置PC和手机主题 + 2025.05.15 1. 【后台 - 分类列表】,添加了添加分类按钮 2. 删除分类时,如果下面存在子分类,则不允许删除 diff --git a/templates/admin/add_link.php b/templates/admin/add_link.php index d4fa317..cd050ff 100755 --- a/templates/admin/add_link.php +++ b/templates/admin/add_link.php @@ -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> diff --git a/templates/admin/header.php b/templates/admin/header.php index c849ad7..9af9916 100755 --- a/templates/admin/header.php +++ b/templates/admin/header.php @@ -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"> diff --git a/templates/admin/setting/theme.php b/templates/admin/setting/theme.php index 26f18d1..917cd43 100755 --- a/templates/admin/setting/theme.php +++ b/templates/admin/setting/theme.php @@ -16,7 +16,40 @@ <!-- 说明提示框END --> <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 @@ <!-- 主题图片 --> <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 @@ <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 @@ </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){ //遍历所有主题,检查是否有更新 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; diff --git a/templates/default2/index.php b/templates/default2/index.php index 1411d40..0f5806c 100644 --- a/templates/default2/index.php +++ b/templates/default2/index.php @@ -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']; ?> - + - + diff --git a/templates/default2/info.json b/templates/default2/info.json old mode 100755 new mode 100644 index 3e1ff11..e705249 --- a/templates/default2/info.json +++ b/templates/default2/info.json @@ -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", "screenshot": "https://v.png.pub/imgs/2024/11/27/c01894e5d9e0d850.png", "demo":"https://nav.rss.ink", diff --git a/version.txt b/version.txt index 9b44705..b13ed31 100755 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v1.1.3-20250515 \ No newline at end of file +v1.1.4-20250709 \ No newline at end of file