diff --git a/.htaccess b/.htaccess deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index abafdef..6543557 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,148 @@ -## SecHub 网安工具集 +# SecHub - 网络安全工具集 -### 技术栈 +![SecHub](assets/imgs/favicon.ico) -PHP: 网页实现 -Json: 存储工具集 +## 项目简介 -### 部署 +SecHub 是一个面向网络安全领域的工具集合系统,提供多种网络安全工具的集中管理。该项目采用 JSON 存储工具信息,通过 PHP 网页界面实现工具的展示与访问,为安全研究人员和从业者提供一站式的工具导航平台。 -直接复制本项目放置于服务器上即可 \ No newline at end of file +## 功能特性 + +- 🛠️ **工具分类管理**:按类别组织网络安全工具,便于查找和使用 +- 🔍 **智能搜索**:支持全局搜索和栏目内搜索,快速定位所需工具 +- 🌓 **主题切换**:支持白天/黑夜模式切换,保护视力 +- 📱 **响应式设计**:适配各种屏幕尺寸,移动端友好 +- 💾 **数据持久化**:使用 SQLite 数据库存储工具信息,提高查询效率 +- 🔄 **自动同步**:JSON 数据变更时自动同步到数据库 +- 📥 **数据导出**:支持下载原始 JSON 数据文件 + +## 技术栈 + +- **后端**: PHP +- **数据库**: SQLite +- **前端**: HTML, CSS, JavaScript +- **数据存储**: JSON 文件 +- **架构**: JSON 驱动架构 + +## 项目结构 + +``` +SecHub/ +├── assets/ +│ ├── css/ +│ │ └── style.css # 样式文件 +│ ├── db/ +│ │ └── sechub.db # SQLite 数据库 +│ ├── imgs/ +│ │ ├── beian.png # 备案图标 +│ │ └── favicon.ico # 网站图标 +│ └── json/ +│ └── template.json # json模板 +│ +├── db.php # 数据库管理类 +├── index.php # 主页面 +└── search.php # 搜索 API +``` + +## 部署方式 + +### 环境要求 + +- PHP 7.0+ +- PDO SQLite 扩展 +- Web 服务器(Apache/Nginx) + +### 快速部署 + +1. 克隆或下载本项目 +2. 将项目文件放置于 Web 服务器根目录 +3. 确保 `assets/db/` 目录具有写入权限 +4. 访问 `index.php` 即可使用 + +### 权限设置 + +确保以下目录具有写入权限: +```bash +chmod 755 assets/db/ +``` + +## 使用说明 + +### 添加新工具 + +在对应的 JSON 文件中添加工具信息,格式如下: + +```json +[ + { + "section": "安全工具" + }, + { + "name": "工具名称", + "url": "https://example.com", + "description": "工具描述" + } +] +``` + +### 搜索功能 + +- **全局搜索**:在顶部搜索框输入关键词,实时显示所有栏目中的匹配结果 +- **栏目搜索**:在每个栏目内的搜索框输入关键词,仅在该栏目中搜索 + +### 主题切换 + +点击右上角的月亮/太阳图标切换白天/黑夜模式,系统会记住您的选择。 + +## 数据格式 + +每个 JSON 文件包含一个数组,第一个元素定义栏目名称,后续元素为具体的工具信息: + +```json +[ + { + "section": "栏目名称" + }, + { + "name": "工具名称", + "url": "工具链接", + "description": "工具描述" + } +] +``` + +## 核心功能说明 + +### 数据库同步机制 + +系统采用智能同步机制,仅在 JSON 文件修改时间超过 5 分钟时才更新数据库,避免频繁读写影响性能。 + +### 搜索算法 + +支持对工具名称、描述和 URL 进行模糊搜索,提供实时的搜索结果反馈。 + +### 响应式设计 + +采用现代 CSS Grid 布局,自动适配不同屏幕尺寸,提供良好的移动端体验。 + + +## 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 + +## 联系方式 + +- 项目主页: [https://git.masonliu.com/MasonLiu/SecHub](https://git.masonliu.com/MasonLiu/SecHub) +- 作者: MasonLiu +- 邮箱: [您的邮箱地址] + +## 备案信息 + +**示例网站:**[sehub.masonliu.com](https://sechub.masonliu.com) + +- ICP 备案号: 蜀ICP备2026025173号 +- 公安备案号: 川公网安备51162302000285号 + +--- + +© 2026 SecHub - Gaming Master Cybersecurity | MasonLiu \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css index 4a440ca..0c32de1 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -5,64 +5,162 @@ } body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: #f5f5f5; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; padding: 20px; - transition: background-color 0.3s ease, color 0.3s ease; + padding-bottom: 40px; + transition: all 0.3s ease; + color: #2d3748; } body.dark-mode { - background: #1a1a1a; - color: #e0e0e0; + background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%); + color: #e2e8f0; } .container { max-width: 1400px; margin: 0 auto; + padding-bottom: 20px; } header { text-align: center; - color: #333; - margin-bottom: 30px; - padding: 20px 0; + margin-bottom: 40px; + padding: 30px 20px; position: relative; - transition: color 0.3s ease; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); + border-radius: 16px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); + transition: all 0.3s ease; + z-index: 10; } body.dark-mode header { - color: #e0e0e0; + background: rgba(26, 32, 44, 0.9); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); } header h1 { - font-size: 2rem; - margin-bottom: 8px; - color: #2c3e50; + font-size: 2.2rem; + margin-bottom: 10px; + color: #2b6cb0; + font-weight: 700; + letter-spacing: -0.5px; transition: color 0.3s ease; } body.dark-mode header h1 { - color: #ecf0f1; + color: #63b3ed; } header p { font-size: 1rem; - color: #666; + color: #718096; transition: color 0.3s ease; } body.dark-mode header p { - color: #b0b0b0; + color: #a0aec0; +} + +/* 搜索框样式 */ +.search-container { + position: relative; + max-width: 600px; + margin: 20px auto 0; +} + +.search-input { + width: 100%; + padding: 12px 20px; + font-size: 1rem; + border: 2px solid #e2e8f0; + border-radius: 12px; + background: white; + transition: all 0.3s ease; + outline: none; +} + +.search-input:focus { + border-color: #4299e1; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); +} + +body.dark-mode .search-input { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; +} + +body.dark-mode .search-input:focus { + border-color: #63b3ed; + box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.1); +} + +.search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 8px; + background: white; + border-radius: 12px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); + max-height: 70vh; + overflow-y: auto; + z-index: 9999; + display: none; + padding: 15px; + border: 2px solid #e2e8f0; +} + +body.dark-mode .search-results { + background: #2d3748; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); + border-color: #4a5568; +} + +/* body.search-active { + padding-bottom: 400px; +} */ + + +.search-result-section { + margin-bottom: 20px; +} + +.search-result-section:last-child { + margin-bottom: 0; +} + +.search-result-title { + font-size: 1rem; + color: #4a5568; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 2px solid #e2e8f0; +} + +body.dark-mode .search-result-title { + color: #a0aec0; + border-bottom-color: #4a5568; +} + +.no-results { + text-align: center; + color: #a0aec0; + padding: 20px; } .theme-toggle { position: absolute; right: 20px; - top: 50%; - transform: translateY(-50%); - background: #fff; - border: 2px solid #e0e0e0; + top: 20px; + background: white; + border: 2px solid #e2e8f0; border-radius: 50%; width: 45px; height: 45px; @@ -72,22 +170,22 @@ body.dark-mode header p { justify-content: center; font-size: 1.3rem; transition: all 0.3s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .theme-toggle:hover { - transform: translateY(-50%) scale(1.1); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); - border-color: #3498db; + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-color: #4299e1; } body.dark-mode .theme-toggle { - background: #2c2c2c; - border-color: #444; + background: #2d3748; + border-color: #4a5568; } body.dark-mode .theme-toggle:hover { - border-color: #f39c12; + border-color: #63b3ed; } .theme-tooltip { @@ -132,48 +230,133 @@ body.dark-mode .theme-toggle:hover { } .section { - margin-bottom: 30px; -} - -.section-title { - color: #2c3e50; - font-size: 1.4rem; - margin-bottom: 15px; - padding: 10px 15px; - border-left: 4px solid #3498db; - background: #fff; - border-radius: 4px; + margin-bottom: 35px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 16px; + padding: 25px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); transition: all 0.3s ease; } +body.dark-mode .section { + background: rgba(26, 32, 44, 0.95); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + gap: 15px; +} + +.section-title-wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.section-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.section-title { + color: #2b6cb0; + font-size: 1.5rem; + font-weight: 600; + margin: 0; + transition: color 0.3s ease; +} + body.dark-mode .section-title { - color: #ecf0f1; - background: #2c2c2c; - border-left-color: #f39c12; + color: #63b3ed; +} + +.section-search { + padding: 8px 15px; + font-size: 0.9rem; + border: 2px solid #e2e8f0; + border-radius: 8px; + background: white; + transition: all 0.3s ease; + outline: none; + min-width: 200px; +} + +.download-btn { + padding: 6px 14px; + font-size: 0.85rem; + border: 2px solid #4299e1; + border-radius: 8px; + background: white; + color: #4299e1; + cursor: pointer; + transition: all 0.3s ease; + outline: none; + font-weight: 500; + white-space: nowrap; +} + +.download-btn:hover { + background: #4299e1; + color: white; + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(66, 153, 225, 0.3); +} + +body.dark-mode .download-btn { + background: #2d3748; + border-color: #63b3ed; + color: #63b3ed; +} + +body.dark-mode .download-btn:hover { + background: #63b3ed; + color: #2d3748; +} + +.section-search:focus { + border-color: #4299e1; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); +} + +body.dark-mode .section-search { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; +} + +body.dark-mode .section-search:focus { + border-color: #63b3ed; + box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.1); } .cards-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 15px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 18px; } .card { background: white; - border-radius: 8px; - padding: 18px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); - transition: all 0.3s ease; + border-radius: 12px; + padding: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; position: relative; overflow: hidden; - border: 1px solid #e0e0e0; + border: 1px solid #e2e8f0; } body.dark-mode .card { - background: #2c2c2c; - border-color: #444; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + background: #2d3748; + border-color: #4a5568; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } .card::before { @@ -182,26 +365,26 @@ body.dark-mode .card { top: 0; left: 0; width: 100%; - height: 3px; - background: linear-gradient(90deg, #3498db, #2980b9); + height: 4px; + background: linear-gradient(90deg, #4299e1, #667eea); transform: scaleX(0); transform-origin: left; transition: transform 0.3s ease; } body.dark-mode .card::before { - background: linear-gradient(90deg, #f39c12, #e67e22); + background: linear-gradient(90deg, #63b3ed, #90cdf4); } .card:hover { - transform: translateY(-3px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); - border-color: #3498db; + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + border-color: #4299e1; } body.dark-mode .card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); - border-color: #f39c12; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); + border-color: #63b3ed; } .card:hover::before { @@ -213,85 +396,88 @@ body.dark-mode .card:hover { } .card-title { - font-size: 1.1rem; - color: #2c3e50; - margin-bottom: 6px; + font-size: 1.15rem; + color: #2d3748; + margin-bottom: 8px; font-weight: 600; transition: color 0.3s ease; } body.dark-mode .card-title { - color: #ecf0f1; + color: #e2e8f0; } .card-link { display: inline-block; - color: #3498db; + color: #4299e1; text-decoration: none; font-size: 0.85rem; word-break: break-all; transition: color 0.2s ease; + font-weight: 500; } body.dark-mode .card-link { - color: #f39c12; + color: #63b3ed; } .card-link:hover { - color: #2980b9; + color: #2b6cb0; text-decoration: underline; } body.dark-mode .card-link:hover { - color: #e67e22; + color: #90cdf4; } .card-description { - color: #666; - line-height: 1.5; + color: #718096; + line-height: 1.6; font-size: 0.9rem; transition: color 0.3s ease; } body.dark-mode .card-description { - color: #b0b0b0; + color: #a0aec0; } .empty-state { text-align: center; - color: #999; - padding: 30px; - background: #fff; - border-radius: 8px; - border: 1px dashed #ddd; + color: #a0aec0; + padding: 40px; + background: rgba(255, 255, 255, 0.5); + border-radius: 12px; + border: 2px dashed #cbd5e0; transition: all 0.3s ease; } body.dark-mode .empty-state { - background: #2c2c2c; - border-color: #444; - color: #888; + background: rgba(45, 55, 72, 0.5); + border-color: #4a5568; + color: #718096; } .empty-state p { - font-size: 0.95rem; + font-size: 1rem; } footer { text-align: center; - color: #999; - margin-top: 40px; - padding: 25px 20px; + color: #718096; + margin-top: 50px; + padding: 30px 20px; font-size: 0.9rem; - background: #fff; - border-top: 1px solid #e0e0e0; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); + border-radius: 16px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); transition: all 0.3s ease; } body.dark-mode footer { - background: #2c2c2c; - border-top-color: #444; - color: #888; + background: rgba(26, 32, 44, 0.9); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + color: #a0aec0; } .footer-content { @@ -301,12 +487,12 @@ body.dark-mode footer { .copyright { margin-bottom: 10px; - color: #666; + color: #4a5568; transition: color 0.3s ease; } body.dark-mode .copyright { - color: #b0b0b0; + color: #a0aec0; } .beian-info { @@ -319,7 +505,7 @@ body.dark-mode .copyright { } .beian-link { - color: #999; + color: #718096; text-decoration: none; transition: color 0.2s ease; display: inline-flex; @@ -328,25 +514,25 @@ body.dark-mode .copyright { } body.dark-mode .beian-link { - color: #888; + color: #a0aec0; } .beian-link:hover { - color: #3498db; + color: #4299e1; } body.dark-mode .beian-link:hover { - color: #f39c12; + color: #63b3ed; } .beian-divider { - color: #ddd; + color: #cbd5e0; user-select: none; transition: color 0.3s ease; } body.dark-mode .beian-divider { - color: #555; + color: #4a5568; } .beian-icon { @@ -355,11 +541,11 @@ body.dark-mode .beian-divider { @media (max-width: 768px) { header h1 { - font-size: 1.6rem; + font-size: 1.8rem; } .section-title { - font-size: 1.2rem; + font-size: 1.3rem; } .cards-grid { @@ -367,7 +553,7 @@ body.dark-mode .beian-divider { } .card { - padding: 15px; + padding: 18px; } .theme-toggle { @@ -375,6 +561,7 @@ body.dark-mode .beian-divider { height: 40px; font-size: 1.1rem; right: 10px; + top: 10px; } .theme-tooltip { @@ -383,6 +570,15 @@ body.dark-mode .beian-divider { bottom: -32px; } + .section-header { + flex-direction: column; + align-items: flex-start; + } + + .section-search { + width: 100%; + } + footer { padding: 20px 15px; font-size: 0.85rem; @@ -404,7 +600,7 @@ body.dark-mode .beian-divider { } header h1 { - font-size: 1.4rem; + font-size: 1.5rem; } header p { @@ -412,12 +608,11 @@ body.dark-mode .beian-divider { } .section-title { - font-size: 1.1rem; - padding: 8px 12px; + font-size: 1.2rem; } .card-title { - font-size: 1rem; + font-size: 1.05rem; } footer { diff --git a/assets/db/sechub.db b/assets/db/sechub.db new file mode 100644 index 0000000..54ff8ba Binary files /dev/null and b/assets/db/sechub.db differ diff --git a/assets/imgs/favicon.ico b/assets/imgs/favicon.ico new file mode 100644 index 0000000..edab233 Binary files /dev/null and b/assets/imgs/favicon.ico differ diff --git a/assets/json/plugin.json b/assets/json/plugin.json deleted file mode 100644 index 6384c95..0000000 --- a/assets/json/plugin.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "section": "插件/非独立小工具" - }, - { - "name": "Nuclei Templates", - "url": "https://github.com/projectdiscovery/nuclei-templates", - "description": "社区驱动的漏洞扫描模板集合,支持多种协议和技术栈" - }, - { - "name": "CVE-Exploit", - "url": "https://github.com/trickest/cve", - "description": "最新CVE漏洞的POC和EXP收集仓库" - }, - { - "name": "Web POC", - "url": "https://github.com/dorkerdevil/Web-Pentest", - "description": "Web应用渗透测试POC脚本合集" - } -] \ No newline at end of file diff --git a/assets/json/poc.json b/assets/json/poc.json deleted file mode 100644 index d456356..0000000 --- a/assets/json/poc.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "section": "POC/EXP" - }, - { - "name": "Nuclei Templates", - "url": "https://github.com/projectdiscovery/nuclei-templates", - "description": "社区驱动的漏洞扫描模板集合,支持多种协议和技术栈" - }, - { - "name": "CVE-Exploit", - "url": "https://github.com/trickest/cve", - "description": "最新CVE漏洞的POC和EXP收集仓库" - }, - { - "name": "Web POC", - "url": "https://github.com/dorkerdevil/Web-Pentest", - "description": "Web应用渗透测试POC脚本合集" - } -] \ No newline at end of file diff --git a/assets/json/template.json b/assets/json/template.json new file mode 100644 index 0000000..a7ad07e --- /dev/null +++ b/assets/json/template.json @@ -0,0 +1,10 @@ +[ + { + "section": "栏目名称" + }, + { + "name": "工具名称", + "url": "工具链接", + "description": "工具描述" + } +] \ No newline at end of file diff --git a/assets/json/tool.json b/assets/json/tool.json deleted file mode 100644 index 020309d..0000000 --- a/assets/json/tool.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - { - "section": "安全工具" - }, - { - "name": "Burp Suite", - "url": "https://portswigger.net/burp", - "description": "专业的Web安全测试工具集,包含代理、扫描器等多种功能模块" - }, - { - "name": "SQLMap", - "url": "https://github.com/sqlmapproject/sqlmap", - "description": "自动化SQL注入检测和利用工具,支持多种数据库类型" - }, - { - "name": "Dirsearch", - "url": "https://github.com/maurosoria/dirsearch", - "description": "高性能Web目录和文件枚举工具,支持多线程和多种输出格式" - }, - { - "name": "Nmap", - "url": "https://nmap.org/", - "description": "网络发现和安全审计工具,支持端口扫描、服务检测等功能" - } -] \ No newline at end of file diff --git a/db.php b/db.php new file mode 100644 index 0000000..369da77 --- /dev/null +++ b/db.php @@ -0,0 +1,353 @@ +dbPath = $dbPath; + $this->jsonDir = $jsonDir; + $this->initDatabase(); + } + + /** + * 初始化数据库连接 + */ + private function initDatabase() { + try { + $this->db = new PDO('sqlite:' . $this->dbPath); + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + + // 创建同步日志表 + $this->createSyncLogTable(); + } catch (PDOException $e) { + error_log("数据库连接失败: " . $e->getMessage()); + throw $e; + } + } + + /** + * 创建同步日志表 + */ + private function createSyncLogTable() { + $sql = "CREATE TABLE IF NOT EXISTS json_sync_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + json_filename TEXT UNIQUE NOT NULL, + table_name TEXT NOT NULL, + last_sync_time DATETIME NOT NULL, + json_file_mtime INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"; + + $this->db->exec($sql); + } + + /** + * 检查并同步JSON数据到数据库 + */ + public function syncJsonToDatabase() { + $jsonFiles = glob($this->jsonDir . '*.json'); + + foreach ($jsonFiles as $file) { + $this->syncSingleFile($file); + } + } + + /** + * 同步单个JSON文件到数据库 + */ + private function syncSingleFile($filePath) { + $filename = basename($filePath); + $tableName = pathinfo($filename, PATHINFO_FILENAME); + + // 检查是否需要更新 + if (!$this->shouldUpdate($filePath, $tableName)) { + return; + } + + // 读取JSON数据 + $data = $this->loadJsonData($filePath); + if (empty($data)) { + return; + } + + // 创建或更新表 + $this->createTable($tableName, $data[0]); + + // 清空旧数据 + $this->clearTable($tableName); + + // 插入新数据(跳过第一个section项) + $items = array_slice($data, 1); + foreach ($items as $item) { + $this->insertItem($tableName, $item, $data[0]['section'] ?? $tableName); + } + + // 更新同步日志 + $this->updateSyncLog($filename, $tableName); + } + + /** + * 更新同步日志 + */ + private function updateSyncLog($filename, $tableName) { + $jsonFile = $this->jsonDir . $filename; + $jsonModified = filemtime($jsonFile); + $syncTime = date('Y-m-d H:i:s'); + + // 检查是否已存在记录 + $sql = "SELECT id FROM json_sync_log WHERE json_filename = :filename"; + $stmt = $this->db->prepare($sql); + $stmt->execute([':filename' => $filename]); + $exists = $stmt->fetch(); + + if ($exists) { + // 更新现有记录 + $sql = "UPDATE json_sync_log + SET table_name = :table_name, + last_sync_time = :sync_time, + json_file_mtime = :mtime + WHERE json_filename = :filename"; + } else { + // 插入新记录 + $sql = "INSERT INTO json_sync_log (json_filename, table_name, last_sync_time, json_file_mtime) + VALUES (:filename, :table_name, :sync_time, :mtime)"; + } + + $stmt = $this->db->prepare($sql); + $stmt->execute([ + ':filename' => $filename, + ':table_name' => $tableName, + ':sync_time' => $syncTime, + ':mtime' => $jsonModified + ]); + } + + /** + * 判断是否需要更新 + */ + private function shouldUpdate($jsonFile, $tableName) { + $filename = basename($jsonFile); + $jsonModified = filemtime($jsonFile); + + // 查询该JSON文件的同步记录 + $sql = "SELECT * FROM json_sync_log WHERE json_filename = :filename"; + $stmt = $this->db->prepare($sql); + $stmt->execute([':filename' => $filename]); + $log = $stmt->fetch(); + + // 如果没有同步记录,需要更新 + if (!$log) { + return true; + } + + // 计算时间差(秒) + $timeDiff = $jsonModified - $log['json_file_mtime']; + + // 如果JSON文件修改时间比记录的晚至少5分钟(300秒),则需要更新 + if ($timeDiff >= 300) { + return true; + } + + // 否则不需要更新 + return false; + } + + + /** + * 加载JSON数据 + */ + private function loadJsonData($filePath) { + if (!file_exists($filePath)) { + return []; + } + + $content = file_get_contents($filePath); + if ($content === false) { + return []; + } + + $data = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + error_log("JSON解析错误: " . json_last_error_msg()); + return []; + } + + return is_array($data) ? $data : []; + } + + /** + * 创建数据表 + */ + private function createTable($tableName, $firstItem) { + // 清理表名,只保留字母、数字和下划线 + $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); + + $sql = "CREATE TABLE IF NOT EXISTS {$tableName} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + section TEXT NOT NULL, + name TEXT NOT NULL, + url TEXT, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"; + + $this->db->exec($sql); + } + + /** + * 清空表数据 + */ + private function clearTable($tableName) { + $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); + $this->db->exec("DELETE FROM {$tableName}"); + } + + /** + * 插入数据项 + */ + private function insertItem($tableName, $item, $section) { + $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); + + $sql = "INSERT INTO {$tableName} (section, name, url, description) + VALUES (:section, :name, :url, :description)"; + + $stmt = $this->db->prepare($sql); + $stmt->execute([ + ':section' => $section, + ':name' => $item['name'] ?? '', + ':url' => $item['url'] ?? '', + ':description' => $item['description'] ?? '' + ]); + } + + /** + * 全局搜索 + */ + public function globalSearch($keyword) { + if (empty($keyword)) { + return []; + } + + $tables = $this->getAllTables(); + $results = []; + + foreach ($tables as $table) { + $tableName = $table['name']; + $sql = "SELECT *, '{$tableName}' as source_table FROM {$tableName} + WHERE name LIKE :keyword + OR description LIKE :keyword + OR url LIKE :keyword + ORDER BY name"; + + $stmt = $this->db->prepare($sql); + $stmt->execute([':keyword' => '%' . $keyword . '%']); + $items = $stmt->fetchAll(); + + if (!empty($items)) { + $results[$tableName] = [ + 'section' => $items[0]['section'] ?? $tableName, + 'items' => $items + ]; + } + } + + return $results; + } + + /** + * 按栏目搜索 + */ + public function searchBySection($section, $keyword) { + if (empty($keyword)) { + return []; + } + + $tables = $this->getAllTables(); + $results = []; + + foreach ($tables as $table) { + $tableName = $table['name']; + $sql = "SELECT * FROM {$tableName} + WHERE section = :section + AND (name LIKE :keyword + OR description LIKE :keyword + OR url LIKE :keyword) + ORDER BY name"; + + $stmt = $this->db->prepare($sql); + $stmt->execute([ + ':section' => $section, + ':keyword' => '%' . $keyword . '%' + ]); + $items = $stmt->fetchAll(); + + if (!empty($items)) { + $results[] = $items; + } + } + + return array_merge(...$results); + } + + /** + * 获取所有栏目配置 + */ + public function getSectionsConfig() { + $tables = $this->getAllTables(); + $sections = []; + + foreach ($tables as $table) { + $tableName = $table['name']; + $sql = "SELECT DISTINCT section FROM {$tableName} LIMIT 1"; + $stmt = $this->db->query($sql); + $row = $stmt->fetch(); + + if ($row) { + $sections[$tableName] = [ + 'title' => $row['section'], + 'table' => $tableName + ]; + } + } + + return $sections; + } + + /** + * 获取指定栏目的所有项目 + */ + public function getItemsBySection($tableName) { + $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); + $sql = "SELECT * FROM {$tableName} ORDER BY name"; + $stmt = $this->db->query($sql); + return $stmt->fetchAll(); + } + + /** + * 获取所有表名 + */ + private function getAllTables() { + $sql = "SELECT name FROM sqlite_master + WHERE type='table' + AND name NOT LIKE 'sqlite_%' + AND name != 'json_sync_log' + ORDER BY name"; + $stmt = $this->db->query($sql); + return $stmt->fetchAll(); + } + + /** + * 关闭数据库连接 + */ + public function close() { + $this->db = null; + } +} \ No newline at end of file diff --git a/index.php b/index.php index d166f61..c132862 100644 --- a/index.php +++ b/index.php @@ -3,63 +3,18 @@ * SecHub - 网络安全工具导航页 */ -// 定义JSON文件路径 +// 定义路径 $jsonDir = __DIR__ . '/assets/json/'; +$dbDir = __DIR__ . '/assets/db/'; +$dbPath = $dbDir . 'sechub.db'; -/** - * 读取JSON文件并返回数据 - * @param string $filePath JSON文件路径 - * @return array 解析后的数据数组 - */ -function loadJsonData($filePath) { - if (!file_exists($filePath)) { - return []; - } - - $content = file_get_contents($filePath); - if ($content === false) { - return []; - } - - $data = json_decode($content, true); - if (json_last_error() !== JSON_ERROR_NONE) { - error_log("JSON解析错误: " . json_last_error_msg()); - return []; - } - - return is_array($data) ? $data : []; +// 确保数据库目录存在 +if (!is_dir($dbDir)) { + mkdir($dbDir, 0755, true); } -/** - * 获取所有JSON文件并自动识别栏目配置 - * @param string $jsonDir JSON目录路径 - * @return array 栏目配置数组 - */ -function getSectionsConfig($jsonDir) { - $sections = []; - $jsonFiles = glob($jsonDir . '*.json'); - - foreach ($jsonFiles as $file) { - $filename = basename($file); - $data = loadJsonData($file); - - if (!empty($data) && isset($data[0]['section'])) { - $sections[$filename] = [ - 'title' => $data[0]['section'], - 'file' => $filename, - 'items' => array_slice($data, 1) - ]; - } else { - $sections[$filename] = [ - 'title' => pathinfo($filename, PATHINFO_FILENAME), - 'file' => $filename, - 'items' => $data - ]; - } - } - - return $sections; -} +// 引入数据库类 +require_once __DIR__ . '/db.php'; /** * 渲染卡片HTML @@ -81,8 +36,17 @@ function renderCard($item) { "; } -// 自动获取栏目配置 -$sections = getSectionsConfig($jsonDir); +// 初始化数据库并同步数据 +try { + $database = new SecHubDatabase($dbPath, $jsonDir); + $database->syncJsonToDatabase(); + + // 获取栏目配置 + $sections = $database->getSectionsConfig(); +} catch (Exception $e) { + error_log("数据库初始化失败: " . $e->getMessage()); + $sections = []; +} ?> @@ -90,6 +54,7 @@ $sections = getSectionsConfig($jsonDir); + SecHub - 网络安全工具集 @@ -98,28 +63,57 @@ $sections = getSectionsConfig($jsonDir);

SecHub 网安工具集

一站式网络安全工具与资源导航平台

+ + +
+ +
+
+
- $config): ?> + $config): ?> getItemsBySection($key); + // 获取对应的JSON文件名 + $jsonFile = $key . '.json'; ?> -
-

- -

+
+
+
+

+ +

+ + +
+ + +
-

暂无数据,请在 中添加项目

+

暂无数据

-
+
@@ -145,6 +139,152 @@ $sections = getSectionsConfig($jsonDir);
- + \ No newline at end of file diff --git a/nginx.htaccess b/nginx.htaccess deleted file mode 100644 index e69de29..0000000 diff --git a/search.php b/search.php new file mode 100644 index 0000000..83c3bfa --- /dev/null +++ b/search.php @@ -0,0 +1,44 @@ +globalSearch($keyword); + echo json_encode($results, JSON_UNESCAPED_UNICODE); + } elseif ($action === 'section') { + // 栏目搜索 + $section = $_GET['section'] ?? ''; + $results = $database->searchBySection($section, $keyword); + echo json_encode($results, JSON_UNESCAPED_UNICODE); + } else { + echo json_encode([]); + } + + $database->close(); +} catch (Exception $e) { + error_log("搜索失败: " . $e->getMessage()); + echo json_encode(['error' => '搜索失败']); +} \ No newline at end of file