Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92ca92bee6 | |||
| 64c82a2406 | |||
| e44ef813a1 | |||
| 9f3b45a4c7 | |||
| b1af0c7d72 | |||
| 421640221b | |||
| f6c6e22b0d | |||
| 51653483b9 | |||
| a9787b0add | |||
| 6cdbf92fb0 | |||
| fdb40680d3 | |||
| 6746325bf2 | |||
| 05a8bff1e0 | |||
| 874ff5b3f6 | |||
| 2230cfa52b | |||
| 83c581364d |
42
.github/workflows/maven.yml
vendored
@ -12,13 +12,41 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 8
|
||||
uses: actions/setup-java@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '8'
|
||||
distribution: 'adopt'
|
||||
cache: maven
|
||||
java-version: '21'
|
||||
distribution: 'temurin' # 使用 Eclipse Temurin (AdoptOpenJDK 的继任者)
|
||||
cache: 'maven'
|
||||
|
||||
- name: Cache Maven packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn -B package --file pom.xml
|
||||
run: mvn -B package -Dmaven.test.skip=true --file pom.xml
|
||||
|
||||
- name: Upload Linux distribution package
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kkfileview-linux
|
||||
path: server/target/*.tar.gz
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Windows distribution package
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kkfileview-windows
|
||||
path: server/target/*.zip
|
||||
retention-days: 7
|
||||
|
||||
94
README.cn.md
@ -53,80 +53,81 @@
|
||||
#### 1. 文本预览
|
||||
支持所有类型的文本文档预览, 由于文本文档类型过多,无法全部枚举,默认开启的类型如下 txt,html,htm,asp,jsp,xml,xbrl,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd
|
||||
文本预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 2. 图片预览
|
||||
支持jpg,jpeg,png,gif等图片预览(翻转,缩放,镜像),预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 3. word文档预览
|
||||
支持doc,docx文档预览,word预览有两种模式:一种是每页word转为图片预览,另一种是整个word文档转成pdf,再预览pdf。两种模式的适用场景如下
|
||||
* 图片预览:word文件大,前台加载整个pdf过慢
|
||||
* pdf预览:内网访问,加载pdf快
|
||||
图片预览模式预览效果如下
|
||||

|
||||

|
||||
pdf预览模式预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 4. ppt文档预览
|
||||
支持ppt,pptx文档预览,和word文档一样,有两种预览模式
|
||||
图片预览模式预览效果如下
|
||||

|
||||

|
||||
pdf预览模式预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 5. pdf文档预览
|
||||
支持pdf文档预览,和word文档一样,有两种预览模式
|
||||
图片预览模式预览效果如下
|
||||

|
||||

|
||||
pdf预览模式预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 6. excel文档预览
|
||||
支持xls,xlsx文档预览,预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 7. 压缩文件预览
|
||||
支持zip,rar,jar,tar,gzip等压缩包,预览效果如下
|
||||

|
||||

|
||||
可点击压缩包中的文件名,直接预览文件,预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 8. 多媒体文件预览
|
||||
理论上支持所有的视频、音频文件,由于无法枚举所有文件格式,默认开启的类型如下
|
||||
mp3,wav,mp4,flv
|
||||
视频预览效果如下
|
||||

|
||||

|
||||
音频预览效果如下
|
||||

|
||||

|
||||
|
||||
#### 9. CAD文档预览
|
||||
支持CAD dwg文档预览,和word文档一样,有两种预览模式
|
||||
图片预览模式预览效果如下
|
||||

|
||||

|
||||
pdf预览模式预览效果如下
|
||||

|
||||
考虑说明篇幅原因,就不贴其他格式文件的预览效果了,感兴趣的可以参考下面的实例搭建下
|
||||

|
||||
|
||||
#### 10. Excel文件纯前端渲染效果
|
||||
|
||||

|
||||

|
||||
|
||||
#### 11. 流程图bpmn文件预览效果
|
||||
|
||||

|
||||

|
||||
|
||||
#### 12. 3D模型文件预览效果:
|
||||
|
||||

|
||||

|
||||
|
||||
#### 13. dcm医疗数位影像文件预览效果:
|
||||
|
||||

|
||||

|
||||
|
||||
#### 14. drawio流程图预览效果:
|
||||
|
||||

|
||||

|
||||
|
||||
考虑说明篇幅原因,就不贴其他格式文件的预览效果了,感兴趣的可以参考下面的实例搭建下
|
||||
|
||||
### 快速开始
|
||||
> 项目使用技术
|
||||
@ -148,7 +149,56 @@ pdf预览模式预览效果如下
|
||||
|
||||
### 历史更新记录
|
||||
|
||||
#### > 2023年07月05日,v4.3 版本发布 :
|
||||
#### > 2025年01月16日,v4.4.0 版本发布 :
|
||||
|
||||
### 新增功能
|
||||
1. xlsx 新增支持打印功能
|
||||
2. 配置文件新增启用 GZIP 压缩
|
||||
3. CAD 格式新增支持转换成 SVG 和 TIF 格式,新增超时结束、线程管理
|
||||
4. 新增删除文件使用验证码校验
|
||||
5. 新增 xbrl 格式预览支持
|
||||
6. PDF 预览新增控制签名、绘图、插图控制、搜索定位页码、定义显示内容等功能
|
||||
7. 新增 CSV 格式前端解析支持
|
||||
8. 新增 ARM64 下的 Docker 镜像支持
|
||||
9. 新增 Office 预览支持转换超时属性设置功能
|
||||
10. 新增预览文件 host 黑名单机制
|
||||
|
||||
### 优化
|
||||
1. 优化 OFD 移动端预览 页面不自适应
|
||||
2. 更新 xlsx 前端解析组件,加速解析速度
|
||||
3. 升级 CAD 组件
|
||||
4. office 功能调整,支持批注、转换页码限制、生成水印等功能
|
||||
5. 升级 markdown 组件
|
||||
6. 升级 dcm 解析组件
|
||||
7. 升级 PDF.JS 解析组件
|
||||
8. 更换视频播放插件为 ckplayer
|
||||
9. tif 解析更加智能化,支持被修改的图片格式
|
||||
10. 针对大小文本文件检测字符编码的正确率,处理并发隐患
|
||||
11. 重构下载文件的代码,新增通用的文件服务器认证访问的设计
|
||||
12. 更新 bootstrap 组件,并精简掉不需要的文件
|
||||
13. 更新 epub 版本,优化 epub 显示效果
|
||||
14. 解决定时清除缓存时,对于多媒体类型文件,只删除了磁盘缓存文件
|
||||
15. 自动检测已安装 Office 组件,增加 LibreOffice 7.5 & 7.6 版本默认路径
|
||||
16. 修改 drawio 默认为预览模式
|
||||
17. 新增 PDF 线程管理、超时管理、内存缓存管理,更新 PDF 解析组件版本
|
||||
18. 优化 Dockerfile,支持真正的跨平台构建镜像
|
||||
|
||||
### 修复
|
||||
1. 修复 forceUpdatedCache 属性设置,但本地缓存文件不更新的问题
|
||||
2. 修复 PDF 解密加密文件转换成功后后台报错的问题
|
||||
3. 修复 BPMN 不支持跨域的问题
|
||||
4. 修复压缩包二级反代特殊符号错误问题
|
||||
5. 修复视频跨域配置导致视频无法预览的问题
|
||||
6. 修复 TXT 文本类分页二次加载问题
|
||||
7. 修复 Drawio 缺少 Base64 组件的问题
|
||||
8. 修复 Markdown 被转义问题
|
||||
9. 修复 EPUB 跨域报错问题
|
||||
10. 修复 URL 特殊符号问题
|
||||
11. 修复压缩包穿越漏洞
|
||||
12. 修复压缩获取路径错误、图片合集路径错误、水印问题等 BUG
|
||||
13. 修复前端解析 XLSX 包含 EMF 格式文件错误问题
|
||||
|
||||
#### > 2023年07月05日,v4.3.0 版本发布 :
|
||||
|
||||
#### 新增功能:
|
||||
1. 新增dcm等医疗数位影像预
|
||||
|
||||
170
SECURITY_CONFIG.md
Normal file
@ -0,0 +1,170 @@
|
||||
# kkFileView 安全配置指南
|
||||
|
||||
## ⚠️ 重要安全更新
|
||||
|
||||
从 4.4.0 之后版本开始,kkFileView 增强了安全性,默认拒绝所有未配置的外部文件预览请求,以防止 SSRF(服务器端请求伪造)攻击。
|
||||
|
||||
## 🔒 安全配置说明
|
||||
|
||||
### 1. 信任主机白名单配置(推荐)
|
||||
|
||||
在 `application.properties` 中配置允许预览的域名:
|
||||
|
||||
```properties
|
||||
# 方式1:通过配置文件
|
||||
trust.host = kkview.cn,yourdomain.com,cdn.example.com
|
||||
|
||||
# 方式2:通过环境变量
|
||||
KK_TRUST_HOST=kkview.cn,yourdomain.com,cdn.example.com
|
||||
```
|
||||
|
||||
**示例场景**:
|
||||
- 只允许预览来自 `oss.aliyuncs.com` 和 `cdn.example.com` 的文件
|
||||
```properties
|
||||
trust.host = oss.aliyuncs.com,cdn.example.com
|
||||
```
|
||||
|
||||
### 2. 允许所有主机(不推荐,仅测试环境)
|
||||
|
||||
```properties
|
||||
trust.host = *
|
||||
```
|
||||
|
||||
⚠️ **警告**:此配置会允许访问任意外部地址,存在安全风险,仅应在测试环境使用!
|
||||
|
||||
### 3. 黑名单配置(高级)
|
||||
|
||||
禁止特定域名或内网地址:
|
||||
|
||||
```properties
|
||||
# 禁止访问内网地址(强烈推荐)
|
||||
not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.*,169.254.*
|
||||
|
||||
# 禁止特定恶意域名
|
||||
not.trust.host = malicious-site.com,spam-domain.net
|
||||
```
|
||||
|
||||
**优先级**:黑名单 > 白名单
|
||||
|
||||
### 4. Docker 环境配置
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-e KK_TRUST_HOST=yourdomain.com,cdn.example.com \
|
||||
-e KK_NOT_TRUST_HOST=localhost,127.0.0.1 \
|
||||
-p 8012:8012 \
|
||||
keking/kkfileview:4.4.0
|
||||
```
|
||||
|
||||
## 🛡️ 安全最佳实践
|
||||
|
||||
### ✅ 推荐配置
|
||||
|
||||
```properties
|
||||
# 1. 明确配置信任主机白名单
|
||||
trust.host = your-cdn.com,your-storage.com
|
||||
|
||||
# 2. 配置黑名单防止内网访问
|
||||
not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.*
|
||||
|
||||
# 3. 禁用文件上传(生产环境)
|
||||
file.upload.disable = true
|
||||
|
||||
# 4. 配置基础URL(使用反向代理时)
|
||||
base.url = https://preview.yourdomain.com
|
||||
```
|
||||
|
||||
### ❌ 不推荐配置
|
||||
|
||||
```properties
|
||||
# 危险:允许所有主机访问
|
||||
trust.host = *
|
||||
|
||||
# 危险:启用文件上传(生产环境)
|
||||
file.upload.disable = false
|
||||
```
|
||||
|
||||
## 🔍 配置验证
|
||||
|
||||
### 测试白名单是否生效
|
||||
|
||||
1. 配置白名单:
|
||||
```properties
|
||||
trust.host = kkview.cn
|
||||
```
|
||||
|
||||
2. 尝试预览白名单内的文件:
|
||||
```
|
||||
http://localhost:8012/onlinePreview?url=https://kkview.cn/test.pdf
|
||||
✅ 应该可以正常预览
|
||||
```
|
||||
|
||||
3. 尝试预览白名单外的文件:
|
||||
```
|
||||
http://localhost:8012/onlinePreview?url=https://other-domain.com/test.pdf
|
||||
❌ 应该被拒绝,显示"不信任的文件源"
|
||||
```
|
||||
|
||||
### 测试黑名单是否生效
|
||||
|
||||
1. 配置黑名单:
|
||||
```properties
|
||||
not.trust.host = localhost,127.0.0.1
|
||||
```
|
||||
|
||||
2. 尝试访问本地文件:
|
||||
```
|
||||
http://localhost:8012/getCorsFile?urlPath=http://127.0.0.1:8080/admin
|
||||
❌ 应该被拒绝
|
||||
```
|
||||
|
||||
## 📋 常见问题
|
||||
|
||||
### Q1: 升级后无法预览文件了?
|
||||
|
||||
**原因**:新版本默认拒绝未配置的主机。
|
||||
|
||||
**解决**:在配置文件中添加信任主机列表:
|
||||
```properties
|
||||
trust.host = your-file-server.com
|
||||
```
|
||||
|
||||
### Q2: 如何临时恢复旧版本行为?
|
||||
|
||||
**不推荐**,但如果确实需要:
|
||||
```properties
|
||||
trust.host = *
|
||||
```
|
||||
|
||||
### Q3: 配置了白名单但还是无法访问?
|
||||
|
||||
检查以下几点:
|
||||
1. 域名是否完全匹配(区分大小写)
|
||||
2. 是否配置了黑名单,黑名单优先级更高
|
||||
3. 查看日志中的 WARNING 信息
|
||||
4. 确认环境变量是否正确设置
|
||||
|
||||
### Q4: 如何允许子域名?
|
||||
|
||||
目前不支持通配符域名匹配,需要明确列出每个子域名:
|
||||
```properties
|
||||
trust.host = cdn.example.com,api.example.com,storage.example.com
|
||||
```
|
||||
|
||||
## 🚨 安全事件响应
|
||||
|
||||
如果发现可疑的预览请求:
|
||||
|
||||
1. 检查日志文件,搜索 "拒绝访问主机" 关键字
|
||||
2. 确认 `trust.host` 配置是否合理
|
||||
3. 检查是否有异常的网络请求
|
||||
4. 如发现攻击行为,及时更新黑名单配置
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
- GitHub Issues: https://github.com/kekingcn/kkFileView/issues
|
||||
- Gitee Issues: https://gitee.com/kekingcn/file-online-preview/issues
|
||||
|
||||
---
|
||||
|
||||
**安全提示**:定期检查和更新信任主机列表,遵循最小权限原则。
|
||||
BIN
doc/img/preview/preview-3ds.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
doc/img/preview/preview-audio.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
doc/img/preview/preview-bpmn.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
doc/img/preview/preview-cad-image.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
doc/img/preview/preview-cad-pdf.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
doc/img/preview/preview-dcm.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
doc/img/preview/preview-doc-image.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
doc/img/preview/preview-doc-pdf.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
doc/img/preview/preview-drawio.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
doc/img/preview/preview-image.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
doc/img/preview/preview-pdf-image.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
doc/img/preview/preview-pdf-pdf.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
doc/img/preview/preview-ppt-image.png
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
doc/img/preview/preview-ppt-pdf.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
doc/img/preview/preview-text.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
doc/img/preview/preview-video.png
Normal file
|
After Width: | Height: | Size: 469 KiB |
BIN
doc/img/preview/preview-xls.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
doc/img/preview/preview-xlsx-web.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
doc/img/preview/preview-xmind.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
doc/img/preview/preview-zip-inner.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
doc/img/preview/preview-zip.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
@ -5,7 +5,7 @@ RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.li
|
||||
sed -i 's@//ports.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources &&\
|
||||
apt-get update &&\
|
||||
export DEBIAN_FRONTEND=noninteractive &&\
|
||||
apt-get install -y --no-install-recommends openjdk-8-jre tzdata locales xfonts-utils fontconfig libreoffice-nogui &&\
|
||||
apt-get install -y --no-install-recommends openjdk-21-jre tzdata locales xfonts-utils fontconfig libreoffice-nogui &&\
|
||||
echo 'Asia/Shanghai' > /etc/timezone &&\
|
||||
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&\
|
||||
localedef -i zh_CN -c -f UTF-8 -A /usr/share/locale/locale.alias zh_CN.UTF-8 &&\
|
||||
|
||||
7
pom.xml
@ -9,14 +9,14 @@
|
||||
<version>4.4.0</version>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<java.version>21</java.version>
|
||||
<jodconverter.version>4.4.6</jodconverter.version>
|
||||
<spring.boot.version>2.4.2</spring.boot.version>
|
||||
<spring.boot.version>3.5.6</spring.boot.version>
|
||||
<poi.version>5.2.2</poi.version>
|
||||
<xdocreport.version>1.0.6</xdocreport.version>
|
||||
<xstream.version>1.4.20</xstream.version>
|
||||
<junrar.version>7.5.5</junrar.version>
|
||||
<redisson.version>3.2.0</redisson.version>
|
||||
<redisson.version>3.22.0</redisson.version>
|
||||
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
|
||||
<jchardet.version>1.0</jchardet.version>
|
||||
<antlr.version>2.7.7</antlr.version>
|
||||
@ -43,6 +43,7 @@
|
||||
|
||||
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
@ -44,21 +44,16 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<!-- web end -->
|
||||
|
||||
<!-- poi start -->
|
||||
@ -100,9 +95,8 @@
|
||||
</dependency>
|
||||
<!-- poi start -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>${httpcomponents.version}</version>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 对 rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; -->
|
||||
@ -182,6 +176,12 @@
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>${pdfbox.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
@ -327,6 +327,14 @@
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<parameters>true</parameters>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
|
||||
@ -22,6 +22,14 @@ spring.freemarker.expose-request-attributes = true
|
||||
spring.freemarker.expose-session-attributes = true
|
||||
spring.freemarker.request-context-attribute = request
|
||||
spring.freemarker.suffix = .ftl
|
||||
# Spring Boot Actuator 健康检查配置
|
||||
# 开启健康检查端点
|
||||
management.endpoints.web.exposure.include=health,info,metrics
|
||||
# 显示详细的健康检查信息(生产环境建议设置为when-authorized)
|
||||
management.endpoint.health.show-details=always
|
||||
# 启用健康检查组件
|
||||
management.health.defaults.enabled=true
|
||||
|
||||
|
||||
# office设置
|
||||
#openoffice或LibreOffice home路径
|
||||
@ -78,11 +86,25 @@ cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}
|
||||
#提供预览服务的地址,默认从请求url读,如果使用nginx等反向代理,需要手动设置
|
||||
#base.url = https://file.keking.cn
|
||||
base.url = ${KK_BASE_URL:default}
|
||||
#信任站点,多个用','隔开,设置了之后,会限制只能预览来自信任站点列表的文件,默认不限制
|
||||
#trust.host = kkview.cn
|
||||
|
||||
# ========== 安全配置(重要)==========
|
||||
# 信任站点白名单配置,多个用','隔开
|
||||
# ⚠️ 安全提示:为防止SSRF攻击,强烈建议配置信任主机白名单
|
||||
# ⚠️ 如果不配置,系统将默认拒绝所有外部文件预览请求
|
||||
#
|
||||
# 配置示例:
|
||||
# trust.host = kkview.cn,yourdomain.com,cdn.example.com
|
||||
#
|
||||
# 如果需要允许所有域名(不推荐,仅用于测试环境),请设置为:
|
||||
# trust.host = *
|
||||
#
|
||||
# 当前配置:
|
||||
trust.host = ${KK_TRUST_HOST:default}
|
||||
#不信任站点,多个用','隔开,设置了之后,会限制来自不信任站点列表的文件,默认不限制
|
||||
#not.trust.host = kkview.cn
|
||||
|
||||
# 不信任站点黑名单配置,多个用','隔开
|
||||
# 黑名单优先级高于白名单,设置后将禁止预览来自这些站点的文件
|
||||
# 建议配置:禁止访问内网地址和本地地址
|
||||
# not.trust.host = localhost,127.0.0.1,0.0.0.0,192.168.*,10.*,172.16.*
|
||||
not.trust.host= ${KK_NOT_TRUST_HOST:default}
|
||||
#文本类型,默认如下,可自定义添加
|
||||
simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
|
||||
@ -159,10 +181,9 @@ watermark.height = ${WATERMARK_HEIGHT:80}
|
||||
#水印倾斜度数,要求设置在大于等于0,小于90
|
||||
watermark.angle = ${WATERMARK_ANGLE:10}
|
||||
|
||||
|
||||
#首页功能设置
|
||||
#是否禁用首页文件上传
|
||||
file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:false}
|
||||
file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:true}
|
||||
# 备案信息,默认为空
|
||||
beian = ${KK_BEIAN:default}
|
||||
#禁止上传类型
|
||||
|
||||
@ -308,7 +308,8 @@ public class ConfigConstants {
|
||||
if (DEFAULT_VALUE.equalsIgnoreCase(trustHost)) {
|
||||
return new CopyOnWriteArraySet<>();
|
||||
} else {
|
||||
String[] trustHostArray = trustHost.toLowerCase().split(",");
|
||||
// 去除空格并转小写
|
||||
String[] trustHostArray = trustHost.toLowerCase().replaceAll("\\s+", "").split(",");
|
||||
return new CopyOnWriteArraySet<>(Arrays.asList(trustHostArray));
|
||||
}
|
||||
}
|
||||
@ -426,7 +427,7 @@ public class ConfigConstants {
|
||||
return fileUploadDisable;
|
||||
}
|
||||
|
||||
@Value("${file.upload.disable:false}")
|
||||
@Value("${file.upload.disable:true}")
|
||||
public void setFileUploadDisable(Boolean fileUploadDisable) {
|
||||
setFileUploadDisableValue(fileUploadDisable);
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -50,26 +50,21 @@ public class RedissonConfig {
|
||||
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
|
||||
.setConnectionPoolSize(connectionPoolSize)
|
||||
.setDatabase(database)
|
||||
.setDnsMonitoring(dnsMonitoring)
|
||||
.setDnsMonitoringInterval(dnsMonitoringInterval)
|
||||
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
|
||||
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
|
||||
.setSubscriptionsPerConnection(subscriptionsPerConnection)
|
||||
.setClientName(clientName)
|
||||
.setFailedAttempts(failedAttempts)
|
||||
.setRetryAttempts(retryAttempts)
|
||||
.setRetryInterval(retryInterval)
|
||||
.setReconnectionTimeout(reconnectionTimeout)
|
||||
.setTimeout(timeout)
|
||||
.setConnectTimeout(connectTimeout)
|
||||
.setIdleConnectionTimeout(idleConnectionTimeout)
|
||||
.setPingTimeout(pingTimeout)
|
||||
.setPassword(StringUtils.trimToNull(password));
|
||||
Codec codec=(Codec) ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader()).newInstance();
|
||||
config.setCodec(codec);
|
||||
config.setThreads(thread);
|
||||
config.setEventLoopGroup(new NioEventLoopGroup());
|
||||
config.setUseLinuxNativeEpoll(false);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ public enum FileType {
|
||||
MEDIACONVERT("mediaFilePreviewImpl"),
|
||||
MARKDOWN("markdownFilePreviewImpl"),
|
||||
XML("xmlFilePreviewImpl"),
|
||||
JSON("jsonFilePreviewImpl"),
|
||||
CAD("cadFilePreviewImpl"),
|
||||
TIFF("tiffFilePreviewImpl"),
|
||||
OFD("ofdFilePreviewImpl"),
|
||||
@ -44,12 +45,13 @@ public enum FileType {
|
||||
private static final String[] DCM_TYPES = {"dcm"};
|
||||
private static final String[] DRAWIO_TYPES = {"drawio"};
|
||||
private static final String[] XML_TYPES = {"xml","xbrl"};
|
||||
private static final String[] JSON_TYPES = {"json"};
|
||||
private static final String[] TIFF_TYPES = {"tif", "tiff"};
|
||||
private static final String[] OFD_TYPES = {"ofd"};
|
||||
private static final String[] SVG_TYPES = {"svg"};
|
||||
private static final String[] CAD_TYPES = {"dwg", "dxf", "dwf", "iges", "igs", "dwt", "dng", "ifc", "dwfx", "stl", "cf2", "plt"};
|
||||
private static final String[] SSIM_TEXT_TYPES = ConfigConstants.getSimText();
|
||||
private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "json", "h", "cpp", "cs", "aspx", "jsp", "sql"};
|
||||
private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "h", "cpp", "cs", "aspx", "jsp", "sql"};
|
||||
private static final String[] MEDIA_TYPES = ConfigConstants.getMedia();
|
||||
public static final String[] MEDIA_CONVERT_TYPES = ConfigConstants.getConvertMedias();
|
||||
private static final Map<String, FileType> FILE_TYPE_MAPPER = new HashMap<>();
|
||||
@ -109,6 +111,9 @@ public enum FileType {
|
||||
for (String xml : XML_TYPES) {
|
||||
FILE_TYPE_MAPPER.put(xml, FileType.XML);
|
||||
}
|
||||
for (String json : JSON_TYPES) {
|
||||
FILE_TYPE_MAPPER.put(json, FileType.JSON);
|
||||
}
|
||||
FILE_TYPE_MAPPER.put("md", FileType.MARKDOWN);
|
||||
FILE_TYPE_MAPPER.put("pdf", FileType.PDF);
|
||||
FILE_TYPE_MAPPER.put("bpmn", FileType.BPMN);
|
||||
|
||||
@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -73,7 +73,7 @@ public class FileConvertQueueTask {
|
||||
TimeUnit.SECONDS.sleep(10);
|
||||
} catch (Exception ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
ex.printStackTrace();
|
||||
logger.error("Failed to sleep after exception", ex);
|
||||
}
|
||||
logger.info("处理预览转换任务异常,url:{}", url, e);
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.net.URLDecoder;
|
||||
@ -178,13 +178,13 @@ public class FileHandlerService implements InitializingBean {
|
||||
sb.append("<script src=\"excel/excel.header.js\" type=\"text/javascript\"></script>");
|
||||
sb.append("<link rel=\"stylesheet\" href=\"excel/excel.css\">");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to read file: {}", outFilePath, e);
|
||||
}
|
||||
// 重新写入文件
|
||||
try (FileOutputStream fos = new FileOutputStream(outFilePath); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
|
||||
writer.write(sb.toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to write file: {}", outFilePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,14 +477,14 @@ public class FileHandlerService implements InitializingBean {
|
||||
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
||||
attribute.setSkipDownLoad(true);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to decode file name: {}", originFileName, e);
|
||||
}
|
||||
}
|
||||
if (UrlEncoderUtils.hasUrlEncoded(originFileName)) { //判断文件名是否转义
|
||||
try {
|
||||
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to decode file name: {}", originFileName, e);
|
||||
}
|
||||
}else {
|
||||
url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义
|
||||
|
||||
@ -26,6 +26,7 @@ public interface FilePreview {
|
||||
String CODE_FILE_PREVIEW_PAGE = "code";
|
||||
String EXEL_FILE_PREVIEW_PAGE = "html";
|
||||
String XML_FILE_PREVIEW_PAGE = "xml";
|
||||
String JSON_FILE_PREVIEW_PAGE = "json";
|
||||
String MARKDOWN_FILE_PREVIEW_PAGE = "markdown";
|
||||
String BPMN_FILE_PREVIEW_PAGE = "bpmn";
|
||||
String DCM_FILE_PREVIEW_PAGE = "dcm";
|
||||
|
||||
@ -15,8 +15,8 @@ import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -7,7 +7,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -9,6 +9,8 @@ import cn.keking.utils.DownloadUtils;
|
||||
import cn.keking.utils.KkFileUtils;
|
||||
import cn.keking.utils.WebUtils;
|
||||
import cn.keking.web.filter.BaseUrlFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.StringUtils;
|
||||
@ -22,6 +24,7 @@ import static cn.keking.service.impl.OfficeFilePreviewImpl.getPreviewType;
|
||||
@Service
|
||||
public class CadFilePreviewImpl implements FilePreview {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CadFilePreviewImpl.class);
|
||||
private static final String OFFICE_PREVIEW_TYPE_IMAGE = "image";
|
||||
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
|
||||
|
||||
@ -55,7 +58,7 @@ public class CadFilePreviewImpl implements FilePreview {
|
||||
try {
|
||||
imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to convert CAD file: {}", filePath, e);
|
||||
}
|
||||
if (imageUrls == null) {
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常,请联系管理员");
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
package cn.keking.service.impl;
|
||||
|
||||
import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.model.FileAttribute;
|
||||
import cn.keking.model.ReturnResponse;
|
||||
import cn.keking.service.FileHandlerService;
|
||||
import cn.keking.service.FilePreview;
|
||||
import cn.keking.utils.DownloadUtils;
|
||||
import cn.keking.utils.KkFileUtils;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
|
||||
/**
|
||||
* @author kl (http://kailing.pub)
|
||||
* @since 2025/01/11
|
||||
* JSON 文件预览处理实现
|
||||
*/
|
||||
@Service
|
||||
public class JsonFilePreviewImpl implements FilePreview {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JsonFilePreviewImpl.class);
|
||||
|
||||
private final FileHandlerService fileHandlerService;
|
||||
private final OtherFilePreviewImpl otherFilePreview;
|
||||
|
||||
public JsonFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
|
||||
this.fileHandlerService = fileHandlerService;
|
||||
this.otherFilePreview = otherFilePreview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||
String fileName = fileAttribute.getName();
|
||||
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||
String filePath = fileAttribute.getOriginFilePath();
|
||||
|
||||
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
|
||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
||||
if (response.isFailure()) {
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
||||
}
|
||||
filePath = response.getContent();
|
||||
if (ConfigConstants.isCacheEnabled()) {
|
||||
fileHandlerService.addConvertedFile(fileName, filePath);
|
||||
}
|
||||
try {
|
||||
String fileData = readJsonFile(filePath, fileName);
|
||||
String escapedData = HtmlUtils.htmlEscape(fileData);
|
||||
String base64Data = Base64.encodeBase64String(escapedData.getBytes(StandardCharsets.UTF_8));
|
||||
model.addAttribute("textData", base64Data);
|
||||
} catch (IOException e) {
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
|
||||
}
|
||||
return JSON_FILE_PREVIEW_PAGE;
|
||||
}
|
||||
|
||||
String fileData = null;
|
||||
try {
|
||||
fileData = HtmlUtils.htmlEscape(readJsonFile(filePath, fileName));
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("读取JSON文件失败: {}", filePath, e);
|
||||
}
|
||||
String base64Data = Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8));
|
||||
model.addAttribute("textData", base64Data);
|
||||
return JSON_FILE_PREVIEW_PAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 JSON 文件,强制使用 UTF-8 编码
|
||||
* JSON 标准规定必须使用 UTF-8 编码
|
||||
*/
|
||||
private String readJsonFile(String filePath, String fileName) throws IOException {
|
||||
File file = new File(filePath);
|
||||
if (KkFileUtils.isIllegalFileName(fileName)) {
|
||||
return null;
|
||||
}
|
||||
if (!file.exists() || file.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// JSON 标准规定使用 UTF-8 编码,不依赖自动检测
|
||||
byte[] bytes = Files.readAllBytes(Paths.get(filePath));
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,8 @@ import org.bytedeco.ffmpeg.global.avcodec;
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@ -26,6 +28,7 @@ import java.io.File;
|
||||
@Service
|
||||
public class MediaFilePreviewImpl implements FilePreview {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
|
||||
private final FileHandlerService fileHandlerService;
|
||||
private final OtherFilePreviewImpl otherFilePreview;
|
||||
private static final String mp4 = "mp4";
|
||||
@ -66,7 +69,7 @@ public class MediaFilePreviewImpl implements FilePreview {
|
||||
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to convert media file: {}", filePath, e);
|
||||
}
|
||||
if (convertedUrl == null) {
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
|
||||
@ -148,7 +151,7 @@ public class MediaFilePreviewImpl implements FilePreview {
|
||||
recorder.record(captured_frame);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to convert video file to mp4: {}", filePath, e);
|
||||
return null;
|
||||
} finally {
|
||||
if (recorder != null) { //关闭
|
||||
|
||||
@ -9,11 +9,17 @@ import cn.keking.utils.DownloadUtils;
|
||||
import cn.keking.utils.EncodingDetects;
|
||||
import cn.keking.utils.KkFileUtils;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
@ -23,10 +29,12 @@ import java.nio.charset.StandardCharsets;
|
||||
@Service
|
||||
public class SimTextFilePreviewImpl implements FilePreview {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SimTextFilePreviewImpl.class);
|
||||
|
||||
private final FileHandlerService fileHandlerService;
|
||||
private final OtherFilePreviewImpl otherFilePreview;
|
||||
|
||||
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview) {
|
||||
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
|
||||
this.fileHandlerService = fileHandlerService;
|
||||
this.otherFilePreview = otherFilePreview;
|
||||
}
|
||||
@ -47,7 +55,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
|
||||
}
|
||||
try {
|
||||
String fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
|
||||
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes()));
|
||||
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (IOException e) {
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
|
||||
}
|
||||
@ -57,9 +65,9 @@ public class SimTextFilePreviewImpl implements FilePreview {
|
||||
try {
|
||||
fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("读取文本文件失败: {}", filePath, e);
|
||||
}
|
||||
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes()));
|
||||
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)));
|
||||
return TXT_FILE_PREVIEW_PAGE;
|
||||
}
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@ import cn.keking.model.ReturnResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.mola.galimatias.GalimatiasParseException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.impl.client.DefaultRedirectStrategy;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
@ -4,6 +4,8 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.extractor.ExtractorFactory;
|
||||
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@ -18,6 +20,7 @@ import java.nio.file.Paths;
|
||||
*/
|
||||
public class OfficeUtils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OfficeUtils.class);
|
||||
private static final String POI_INVALID_PASSWORD_MSG = "password";
|
||||
|
||||
/**
|
||||
@ -49,7 +52,7 @@ public class OfficeUtils {
|
||||
try {
|
||||
propStream.close();//关闭文件输入流
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to close input stream for file: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,7 +79,7 @@ public class OfficeUtils {
|
||||
try {
|
||||
propStream.close();//关闭文件输入流
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to close input stream for file: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package cn.keking.utils;
|
||||
import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.service.ZtreeNodeVo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
@ -15,6 +18,7 @@ import java.util.regex.Pattern;
|
||||
* create : 2023-04-08
|
||||
**/
|
||||
public class RarUtils {
|
||||
private static final Logger logger = LoggerFactory.getLogger(RarUtils.class);
|
||||
private static final String fileDir = ConfigConstants.getFileDir();
|
||||
|
||||
public static byte[] getUTF8BytesFromGBKString(String gbkStr) {
|
||||
@ -55,7 +59,7 @@ public class RarUtils {
|
||||
str = new String(getUTF8BytesFromGBKString(str), StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to convert string encoding: {}", str, e);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
package cn.keking.utils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@ -31,6 +34,8 @@ import java.net.URL;
|
||||
*/
|
||||
public class SimpleEncodingDetects {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SimpleEncodingDetects.class);
|
||||
|
||||
/**
|
||||
* 得到文件的编码
|
||||
* @param content 文件内容
|
||||
@ -65,10 +70,10 @@ public class SimpleEncodingDetects {
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
logger.error("File not found: {}", file, e);
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to read file: {}", file, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
package cn.keking.utils;
|
||||
|
||||
import io.mola.galimatias.GalimatiasParseException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
@ -87,7 +87,7 @@ public class WebUtils {
|
||||
try {
|
||||
urlStr = URLEncoder.encode(urlStr, "UTF-8").replaceAll("\\+", "%20").replaceAll("%3A", ":").replaceAll("%2F", "/").replaceAll("%3F", "?").replaceAll("%26", "&").replaceAll("%3D", "=");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("Failed to encode URL: {}", urlStr, e);
|
||||
}
|
||||
}
|
||||
return urlStr;
|
||||
@ -155,7 +155,7 @@ public class WebUtils {
|
||||
URL urlObj = new URL(url);
|
||||
url = urlObj.getPath().substring(1);
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("Failed to parse file URL: {}", url, e);
|
||||
}
|
||||
}
|
||||
// 因为url的参数中可能会存在/的情况,所以直接url.lastIndexOf("/")会有问题
|
||||
@ -290,7 +290,7 @@ public class WebUtils {
|
||||
* https://github.com/kekingcn/kkFileView/pull/340
|
||||
*/
|
||||
try {
|
||||
return new String(Base64Utils.decodeFromString(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets);
|
||||
return new String(Base64.decodeBase64(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets);
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage().toLowerCase().contains(BASE64_MSG)) {
|
||||
LOGGER.error("url解码异常,接入方法错误未使用BASE64");
|
||||
|
||||
@ -19,9 +19,9 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@ -11,9 +11,9 @@ import cn.keking.utils.WebUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import fr.opensagres.xdocreport.core.io.IOUtils;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.impl.client.DefaultRedirectStrategy;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@ -28,8 +28,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.client.RequestCallback;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.config.WatermarkConfigConstants;
|
||||
import cn.keking.utils.KkFileUtils;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
||||
@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package cn.keking.web.filter;
|
||||
|
||||
|
||||
import javax.servlet.*;
|
||||
import jakarta.servlet.*;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
||||
@ -3,10 +3,10 @@ package cn.keking.web.filter;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.*;
|
||||
import jakarta.servlet.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
@ -35,7 +35,7 @@ public class TrustDirFilter implements Filter {
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
|
||||
this.notTrustDirView = new String(bytes, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("加载notTrustDir.html失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,14 +5,16 @@ import cn.keking.utils.WebUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
@ -22,6 +24,7 @@ import org.springframework.util.FileCopyUtils;
|
||||
*/
|
||||
public class TrustHostFilter implements Filter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TrustHostFilter.class);
|
||||
private String notTrustHostHtmlView;
|
||||
|
||||
@Override
|
||||
@ -32,7 +35,7 @@ public class TrustHostFilter implements Filter {
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
|
||||
this.notTrustHostHtmlView = new String(bytes, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to load notTrustHost.html file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,13 +54,25 @@ public class TrustHostFilter implements Filter {
|
||||
}
|
||||
|
||||
public boolean isNotTrustHost(String host) {
|
||||
// 如果配置了黑名单,优先检查黑名单
|
||||
if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())) {
|
||||
return ConfigConstants.getNotTrustHostSet().contains(host);
|
||||
}
|
||||
|
||||
// 如果配置了白名单,检查是否在白名单中
|
||||
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
|
||||
// 支持通配符 * 表示允许所有主机
|
||||
if (ConfigConstants.getTrustHostSet().contains("*")) {
|
||||
logger.debug("允许所有主机访问(通配符模式): {}", host);
|
||||
return false;
|
||||
}
|
||||
return !ConfigConstants.getTrustHostSet().contains(host);
|
||||
}
|
||||
return false;
|
||||
|
||||
// 安全加固:默认拒绝所有未配置的主机(防止SSRF攻击)
|
||||
// 如果需要允许所有主机,请在配置文件中明确设置 trust.host = *
|
||||
logger.warn("未配置信任主机列表,拒绝访问主机: {},请在配置文件中设置 trust.host 或 KK_TRUST_HOST 环境变量", host);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -2,9 +2,9 @@ package cn.keking.web.filter;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
| < | < | | | | | | | __/ \ / | | | __/ \ V V /
|
||||
|_|\_\ |_|\_\ |_| |_| |_| \___| \/ |_| \___| \_/\_/
|
||||
|
||||
=> Java Version :: ${java.version}
|
||||
=> Spring Boot :: ${spring-boot.version}
|
||||
=> kkFileView :: 4.4.0
|
||||
=> Home site :: https://kkview.cn
|
||||
|
||||
@ -8,7 +8,19 @@
|
||||
*/
|
||||
function initWaterMark() {
|
||||
let watermarkTxt = '${watermarkTxt}';
|
||||
if (watermarkTxt !== '') {
|
||||
if (watermarkTxt === '') {
|
||||
return;
|
||||
}
|
||||
let lastWidth = 0;
|
||||
let lastHeight = 0;
|
||||
const checkResize = () => {
|
||||
const currentWidth = document.documentElement.scrollWidth;
|
||||
const currentHeight = document.documentElement.scrollHeight;
|
||||
// 检测尺寸是否变化
|
||||
if (currentWidth === lastWidth && currentHeight === lastHeight) {
|
||||
return;
|
||||
}
|
||||
// 如果变化了, 重新初始化水印
|
||||
watermark.init({
|
||||
watermark_txt: '${watermarkTxt}',
|
||||
watermark_x: 0,
|
||||
@ -25,7 +37,11 @@
|
||||
watermark_height: ${watermarkHeight},
|
||||
watermark_angle: ${watermarkAngle},
|
||||
});
|
||||
}
|
||||
// 更新存储的宽口大小
|
||||
lastWidth = currentWidth;
|
||||
lastHeight = currentHeight;
|
||||
};
|
||||
setInterval(checkResize, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
384
server/src/main/resources/web/json.ftl
Normal file
@ -0,0 +1,384 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0">
|
||||
<title>JSON文件预览</title>
|
||||
<#include "*/commonHeader.ftl">
|
||||
<script src="js/jquery-3.6.1.min.js" type="text/javascript"></script>
|
||||
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css"/>
|
||||
<script src="bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
|
||||
<script src="js/base64.min.js" type="text/javascript"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
.container {
|
||||
max-width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
#json {
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre {
|
||||
padding: 20px;
|
||||
padding-left: 65px; /* 为行号留出空间 */
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
}
|
||||
.json-key {
|
||||
color: #881391;
|
||||
font-weight: bold;
|
||||
}
|
||||
.json-string {
|
||||
color: #1A1AA6;
|
||||
}
|
||||
.json-number {
|
||||
color: #1C00CF;
|
||||
}
|
||||
.json-boolean {
|
||||
color: #0D22FF;
|
||||
font-weight: bold;
|
||||
}
|
||||
.json-null {
|
||||
color: #808080;
|
||||
font-weight: bold;
|
||||
}
|
||||
.json-toggle {
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.json-toggle:hover {
|
||||
color: #333;
|
||||
}
|
||||
.json-node {
|
||||
display: block;
|
||||
}
|
||||
.line-number {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 55px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<input hidden id="textData" value="${textData}"/>
|
||||
<div class="container">
|
||||
<div class="panel panel-default">
|
||||
<div id="formatted_btn" class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
|
||||
${file.name}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="raw_btn" class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
|
||||
${file.name}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="json" class="panel-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
window.onload = function () {
|
||||
$("#formatted_btn").hide();
|
||||
initWaterMark();
|
||||
loadJsonData();
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 反转义(用于还原后端转义的内容)
|
||||
* 使用浏览器的 DOM 来正确解码所有 HTML 实体
|
||||
*/
|
||||
function htmlUnescape(str) {
|
||||
if (!str || str.length === 0) return "";
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.innerHTML = str;
|
||||
return textarea.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 转义(用于安全显示)
|
||||
*/
|
||||
function htmlEscape(str) {
|
||||
if (!str || str.length === 0) return "";
|
||||
var s = str;
|
||||
s = s.replace(/&/g, "&");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/"/g, """);
|
||||
s = s.replace(/'/g, "'");
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 BOM (Byte Order Mark)
|
||||
*/
|
||||
function removeBOM(str) {
|
||||
if (str.charCodeAt(0) === 0xFEFF) {
|
||||
return str.substring(1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// 全局行号计数器
|
||||
var lineNumber = 1;
|
||||
|
||||
/**
|
||||
* 构建可展开/收起的 JSON 树形结构
|
||||
*/
|
||||
function buildJsonTree(obj, indent, skipLineNumber) {
|
||||
indent = indent || 0;
|
||||
skipLineNumber = skipLineNumber || false;
|
||||
var html = '';
|
||||
var indentStr = ' '.repeat(indent);
|
||||
|
||||
if (obj === null) {
|
||||
return '<span class="json-null">null</span>';
|
||||
}
|
||||
|
||||
if (typeof obj !== 'object') {
|
||||
if (typeof obj === 'string') {
|
||||
// 转义特殊字符,避免换行和制表符破坏布局
|
||||
var escapedStr = obj
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t')
|
||||
.replace(/"/g, '\\"');
|
||||
return '<span class="json-string">"' + htmlEscape(escapedStr) + '"</span>';
|
||||
} else if (typeof obj === 'number') {
|
||||
return '<span class="json-number">' + obj + '</span>';
|
||||
} else if (typeof obj === 'boolean') {
|
||||
return '<span class="json-boolean">' + obj + '</span>';
|
||||
}
|
||||
return htmlEscape(String(obj));
|
||||
}
|
||||
|
||||
var isArray = Array.isArray(obj);
|
||||
var entries = isArray ? obj : Object.keys(obj);
|
||||
var openBracket = isArray ? '[' : '{';
|
||||
var closeBracket = isArray ? ']' : '}';
|
||||
|
||||
if (entries.length === 0) {
|
||||
return openBracket + closeBracket;
|
||||
}
|
||||
|
||||
var nodeId = 'node_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// 如果不跳过行号,说明是新的一行
|
||||
if (!skipLineNumber) {
|
||||
html += '<span class="line-number">' + lineNumber++ + '</span>';
|
||||
}
|
||||
|
||||
html += '<span class="json-toggle" onclick="toggleJsonNode(\'' + nodeId + '\')">▼</span> ';
|
||||
html += openBracket + '\n';
|
||||
html += '<div id="' + nodeId + '" class="json-node">';
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var key = isArray ? i : entries[i];
|
||||
var value = isArray ? entries[i] : obj[entries[i]];
|
||||
|
||||
html += '<span class="line-number">' + lineNumber++ + '</span>';
|
||||
html += indentStr + ' ';
|
||||
if (!isArray) {
|
||||
html += '<span class="json-key">"' + htmlEscape(key) + '"</span>: ';
|
||||
}
|
||||
|
||||
// 如果值是对象或数组,跳过它的行号(因为已经在上面添加了)
|
||||
html += buildJsonTree(value, indent + 1, true);
|
||||
|
||||
if (i < entries.length - 1) {
|
||||
html += ',';
|
||||
}
|
||||
html += '\n';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '<span class="line-number">' + lineNumber++ + '</span>';
|
||||
html += indentStr + closeBracket;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 JSON 节点展开/收起
|
||||
*/
|
||||
function toggleJsonNode(nodeId) {
|
||||
var node = document.getElementById(nodeId);
|
||||
var toggle = event.target;
|
||||
|
||||
if (node.style.display === 'none') {
|
||||
node.style.display = 'block';
|
||||
toggle.textContent = '▼';
|
||||
} else {
|
||||
node.style.display = 'none';
|
||||
toggle.textContent = '▶';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部展开
|
||||
*/
|
||||
function expandAll() {
|
||||
var nodes = document.querySelectorAll('.json-node');
|
||||
var toggles = document.querySelectorAll('.json-toggle');
|
||||
nodes.forEach(function(node) {
|
||||
node.style.display = 'block';
|
||||
});
|
||||
toggles.forEach(function(toggle) {
|
||||
toggle.textContent = '▼';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部收起
|
||||
*/
|
||||
function collapseAll() {
|
||||
var nodes = document.querySelectorAll('.json-node');
|
||||
var toggles = document.querySelectorAll('.json-toggle');
|
||||
nodes.forEach(function(node) {
|
||||
node.style.display = 'none';
|
||||
});
|
||||
toggles.forEach(function(toggle) {
|
||||
toggle.textContent = '▶';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 语法高亮(简单版本,用于原始文本视图)
|
||||
*/
|
||||
function syntaxHighlight(json) {
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||
var cls = 'json-number';
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'json-key';
|
||||
} else {
|
||||
cls = 'json-string';
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'json-boolean';
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'json-null';
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF-8 解码 Base64(正确处理中文等 UTF-8 字符)
|
||||
*/
|
||||
function decodeBase64UTF8(base64Str) {
|
||||
try {
|
||||
// 方法1:使用现代浏览器的 TextDecoder API(推荐)
|
||||
if (typeof TextDecoder !== 'undefined') {
|
||||
var binaryString = window.atob(base64Str);
|
||||
var bytes = new Uint8Array(binaryString.length);
|
||||
for (var i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
}
|
||||
|
||||
// 方法2:降级方案
|
||||
return decodeURIComponent(escape(window.atob(base64Str)));
|
||||
} catch (e) {
|
||||
console.error('Base64 decode error:', e);
|
||||
// 最后降级到 Base64.js 库
|
||||
return Base64.decode(base64Str);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 JSON 数据
|
||||
*/
|
||||
function loadJsonData() {
|
||||
try {
|
||||
var textData = decodeBase64UTF8($("#textData").val());
|
||||
|
||||
// 1. 先反转义 HTML 实体(因为后端已经转义过)
|
||||
textData = htmlUnescape(textData);
|
||||
|
||||
// 2. 移除 BOM
|
||||
textData = removeBOM(textData);
|
||||
|
||||
// 保存原始文本(用于显示时再次转义以保证安全)
|
||||
window.rawText = "<pre style='background-color: #FFFFFF; border: none; margin: 0;'>" + htmlEscape(textData) + "</pre>";
|
||||
|
||||
// 尝试解析并格式化 JSON
|
||||
try {
|
||||
var jsonObj = JSON.parse(textData);
|
||||
|
||||
// 重置行号计数器
|
||||
lineNumber = 1;
|
||||
|
||||
// 构建树形视图
|
||||
var treeHtml = '<div style="padding: 20px;">';
|
||||
treeHtml += '<div style="margin-bottom: 10px;">';
|
||||
treeHtml += '<button onclick="expandAll()" class="btn btn-sm btn-default" style="margin-right: 5px;">全部展开</button>';
|
||||
treeHtml += '<button onclick="collapseAll()" class="btn btn-sm btn-default">全部收起</button>';
|
||||
treeHtml += '</div>';
|
||||
treeHtml += '<pre style="background-color: #f8f9fa; border: none; margin: 0;">';
|
||||
treeHtml += buildJsonTree(jsonObj, 0);
|
||||
treeHtml += '</pre></div>';
|
||||
window.formattedJson = treeHtml;
|
||||
|
||||
// 默认显示树形视图
|
||||
$("#json").html(window.formattedJson);
|
||||
} catch (e) {
|
||||
// 如果不是有效的 JSON,显示错误并回退到原始文本
|
||||
window.formattedJson = "<div class='alert alert-warning'>JSON 解析失败: " + htmlEscape(e.message) + "</div>" + window.rawText;
|
||||
$("#json").html(window.formattedJson);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
$("#json").html("<div class='alert alert-danger'>文件加载失败: " + htmlEscape(e.message) + "</div>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮点击事件
|
||||
*/
|
||||
$(function () {
|
||||
$("#formatted_btn").click(function () {
|
||||
$("#json").html(window.formattedJson);
|
||||
$("#raw_btn").show();
|
||||
$("#formatted_btn").hide();
|
||||
});
|
||||
|
||||
$("#raw_btn").click(function () {
|
||||
$("#json").html(window.rawText);
|
||||
$("#formatted_btn").show();
|
||||
$("#raw_btn").hide();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -148,6 +148,18 @@
|
||||
<input type="file" id="file" name="file" style="float: left; margin: 0 auto; font-size:22px;" placeholder="请选择文件"/>
|
||||
<input type="button" id="fileUploadBtn" class="btn btn-success" value=" 上 传 "/>
|
||||
</form>
|
||||
<#else>
|
||||
<div style="padding: 20px; margin: 10px 0; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 16px;">
|
||||
文件上传功能默认已禁用。如需开启,请通过以下方式配置:
|
||||
<br/>
|
||||
• 配置文件:<code>file.upload.disable=false</code>
|
||||
<br/>
|
||||
• 环境变量:<code>KK_FILE_UPLOAD_DISABLE=false</code>
|
||||
<br/>
|
||||
<strong style="color: #dc3545;">请注意:文件上传限开发环境调试使用,生产环境建议保持关闭状态,避免非法上传导致的安全隐患。</strong>
|
||||
</p>
|
||||
</div>
|
||||
</#if>
|
||||
<table id="table" data-pagination="true"></table>
|
||||
</div>
|
||||
|
||||
@ -21,12 +21,4 @@ public class WebUtilsTests {
|
||||
String out = "https://file.keking.cn/demo/%23hello%26world.txt?param0=0¶m1=1";
|
||||
assert WebUtils.encodeUrlFileName(in).equals(out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodeUrlFullFileNameTestWithParams() {
|
||||
// 测试对URL中使用fullfilename参数的文件名部分进行UTF-8编码
|
||||
String in = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello#0.txt";
|
||||
String out = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello%230.txt";
|
||||
assert WebUtils.encodeUrlFileName(in).equals(out);
|
||||
}
|
||||
}
|
||||
|
||||