|
|
|
|
@ -2,105 +2,173 @@
|
|
|
|
|
# encoding: utf-8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import random
|
|
|
|
|
import string
|
|
|
|
|
import uuid
|
|
|
|
|
from hashlib import sha256
|
|
|
|
|
|
|
|
|
|
import bleach
|
|
|
|
|
import markdown
|
|
|
|
|
import requests
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
from django.contrib.sites.models import Site
|
|
|
|
|
from django.core.cache import cache
|
|
|
|
|
from django.templatetags.static import static
|
|
|
|
|
|
|
|
|
|
import logging # 日志模块:记录操作信息和错误
|
|
|
|
|
import os # 系统操作模块:处理文件路径、目录创建等
|
|
|
|
|
import random # 随机数模块:生成验证码等随机内容
|
|
|
|
|
import string # 字符串模块:提供数字、字母等常量
|
|
|
|
|
import uuid # 唯一模块:生成唯一标识符(用于头像文件名)
|
|
|
|
|
from hashlib import sha256 # 加密模块:提供 SHA256 加密算法
|
|
|
|
|
|
|
|
|
|
import bleach # HTML 清理模块:过滤不安全的 HTML 标签(防 XSS 攻击)
|
|
|
|
|
import markdown # Markdown 解析模块:将 Markdown 文本转为 HTML
|
|
|
|
|
import requests # HTTP 请求模块:下载网络资源(如用户头像)
|
|
|
|
|
from django.conf import settings # Django 配置:获取项目设置(如静态文件路径)
|
|
|
|
|
from django.contrib.sites.models import Site # 站点模型:获取当前站点信息(域名等)
|
|
|
|
|
from django.core.cache import cache # 缓存模块:操作 Django 缓存(获取/设置/删除)
|
|
|
|
|
from django.templatetags.static import static # 静态文件工具:生成静态文件的 URL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化日志对象:指定日志归属为当前模块,便于日志分类
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_max_articleid_commentid():
|
|
|
|
|
"""
|
|
|
|
|
获取当前最大的文章 ID 和评论 ID(用于数据统计或初始化)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
tuple: (最大文章 ID, 最大评论 ID)
|
|
|
|
|
"""
|
|
|
|
|
# 延迟导入模型:避免循环导入问题(工具模块可能被模型模块引用)
|
|
|
|
|
from blog.models import Article
|
|
|
|
|
from comments.models import Comment
|
|
|
|
|
# 返回最新文章和评论的主键(ID)
|
|
|
|
|
return (Article.objects.latest().pk, Comment.objects.latest().pk)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_sha256(str):
|
|
|
|
|
"""
|
|
|
|
|
对字符串进行 SHA256 加密(用于密码加密、唯一标识生成等)
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
str: 待加密的字符串
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: 加密后的 64 位十六进制字符串
|
|
|
|
|
"""
|
|
|
|
|
# 创建 SHA256 加密对象,需先将字符串转为字节流(指定编码 utf-8)
|
|
|
|
|
m = sha256(str.encode('utf-8'))
|
|
|
|
|
# 返回十六进制加密结果
|
|
|
|
|
return m.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cache_decorator(expiration=3 * 60):
|
|
|
|
|
"""
|
|
|
|
|
缓存装饰器:装饰函数,将函数返回值缓存指定时间(默认 3 分钟)
|
|
|
|
|
|
|
|
|
|
作用:减少重复计算或数据库查询,提升性能
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
expiration: 缓存有效期(秒),默认 3 分钟
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
装饰器函数:包装原函数,实现缓存逻辑
|
|
|
|
|
"""
|
|
|
|
|
def wrapper(func):
|
|
|
|
|
def news(*args, **kwargs):
|
|
|
|
|
# 尝试生成缓存键(优先使用视图对象的 get_cache_key 方法)
|
|
|
|
|
try:
|
|
|
|
|
view = args[0]
|
|
|
|
|
key = view.get_cache_key()
|
|
|
|
|
view = args[0] # 若第一个参数是视图对象
|
|
|
|
|
key = view.get_cache_key() # 使用视图自带的缓存键
|
|
|
|
|
except:
|
|
|
|
|
key = None
|
|
|
|
|
key = None # 非视图函数,需自定义缓存键
|
|
|
|
|
|
|
|
|
|
# 若未生成缓存键,则基于函数和参数生成唯一键
|
|
|
|
|
if not key:
|
|
|
|
|
# 将函数、参数转为字符串,确保唯一性
|
|
|
|
|
unique_str = repr((func, args, kwargs))
|
|
|
|
|
|
|
|
|
|
# 对字符串进行 SHA256 加密,生成固定长度的缓存键
|
|
|
|
|
m = sha256(unique_str.encode('utf-8'))
|
|
|
|
|
key = m.hexdigest()
|
|
|
|
|
|
|
|
|
|
# 尝试从缓存获取数据
|
|
|
|
|
value = cache.get(key)
|
|
|
|
|
if value is not None:
|
|
|
|
|
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
|
|
|
|
|
# 缓存命中:返回缓存值(处理 None 的特殊标记)
|
|
|
|
|
if str(value) == '__default_cache_value__':
|
|
|
|
|
return None
|
|
|
|
|
else:
|
|
|
|
|
return value
|
|
|
|
|
else:
|
|
|
|
|
# 缓存未命中:执行原函数获取结果
|
|
|
|
|
logger.debug(
|
|
|
|
|
'cache_decorator set cache:%s key:%s' %
|
|
|
|
|
(func.__name__, key))
|
|
|
|
|
value = func(*args, **kwargs)
|
|
|
|
|
# 缓存结果(用特殊标记表示 None,避免缓存不生效)
|
|
|
|
|
if value is None:
|
|
|
|
|
cache.set(key, '__default_cache_value__', expiration)
|
|
|
|
|
else:
|
|
|
|
|
cache.set(key, value, expiration)
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
return news
|
|
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def expire_view_cache(path, servername, serverport, key_prefix=None):
|
|
|
|
|
'''
|
|
|
|
|
刷新视图缓存
|
|
|
|
|
:param path:url路径
|
|
|
|
|
:param servername:host
|
|
|
|
|
:param serverport:端口
|
|
|
|
|
:param key_prefix:前缀
|
|
|
|
|
:return:是否成功
|
|
|
|
|
主动刷新指定 URL 路径的视图缓存(用于数据更新后清理旧缓存)
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
path: URL 路径(如 '/article/1/')
|
|
|
|
|
servername: 服务器域名(如 'www.example.com')
|
|
|
|
|
serverport: 服务器端口(如 80)
|
|
|
|
|
key_prefix: 缓存键前缀(与视图缓存配置一致)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
bool: 缓存是否成功删除
|
|
|
|
|
'''
|
|
|
|
|
from django.http import HttpRequest
|
|
|
|
|
from django.utils.cache import get_cache_key
|
|
|
|
|
|
|
|
|
|
from django.http import HttpRequest # 延迟导入:避免启动时依赖冲突
|
|
|
|
|
from django.utils.cache import get_cache_key # 获取视图缓存键的工具
|
|
|
|
|
|
|
|
|
|
# 构造模拟请求对象(用于生成缓存键)
|
|
|
|
|
request = HttpRequest()
|
|
|
|
|
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
|
|
|
|
|
request.path = path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 获取该请求对应的缓存键
|
|
|
|
|
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
|
|
|
|
|
if key:
|
|
|
|
|
logger.info('expire_view_cache:get key:{path}'.format(path=path))
|
|
|
|
|
# 若缓存存在,则删除
|
|
|
|
|
if cache.get(key):
|
|
|
|
|
cache.delete(key)
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cache_decorator()
|
|
|
|
|
@cache_decorator() # 应用缓存装饰器:缓存当前站点信息(默认 3 分钟)
|
|
|
|
|
def get_current_site():
|
|
|
|
|
"""
|
|
|
|
|
获取当前站点信息(域名等),从缓存获取以减少数据库查询
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Site: Django Site 模型实例
|
|
|
|
|
"""
|
|
|
|
|
site = Site.objects.get_current()
|
|
|
|
|
return site
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CommonMarkdown:
|
|
|
|
|
"""
|
|
|
|
|
Markdown 解析工具类:将 Markdown 文本转为 HTML,并支持提取目录(TOC)
|
|
|
|
|
"""
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _convert_markdown(value):
|
|
|
|
|
"""
|
|
|
|
|
内部方法:执行 Markdown 转换,返回 HTML 内容和目录
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
value: Markdown 格式的文本
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
tuple: (转换后的 HTML 内容, 目录 HTML)
|
|
|
|
|
"""
|
|
|
|
|
# 初始化 Markdown 解析器,启用扩展:
|
|
|
|
|
# - extra: 支持表格、脚注等扩展语法
|
|
|
|
|
# - codehilite: 代码高亮
|
|
|
|
|
# - toc: 生成目录
|
|
|
|
|
# - tables: 表格支持(extra 已包含,此处冗余可能为兼容)
|
|
|
|
|
md = markdown.Markdown(
|
|
|
|
|
extensions=[
|
|
|
|
|
'extra',
|
|
|
|
|
@ -109,124 +177,227 @@ class CommonMarkdown:
|
|
|
|
|
'tables',
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
body = md.convert(value)
|
|
|
|
|
toc = md.toc
|
|
|
|
|
body = md.convert(value) # 转换文本为 HTML
|
|
|
|
|
toc = md.toc # 提取目录 HTML
|
|
|
|
|
return body, toc
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_markdown_with_toc(value):
|
|
|
|
|
"""
|
|
|
|
|
获取带目录的 Markdown 转换结果
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
value: Markdown 文本
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
tuple: (HTML 内容, 目录 HTML)
|
|
|
|
|
"""
|
|
|
|
|
body, toc = CommonMarkdown._convert_markdown(value)
|
|
|
|
|
return body, toc
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_markdown(value):
|
|
|
|
|
"""
|
|
|
|
|
获取仅包含 HTML 内容的转换结果(忽略目录)
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
value: Markdown 文本
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: 转换后的 HTML 内容
|
|
|
|
|
"""
|
|
|
|
|
body, toc = CommonMarkdown._convert_markdown(value)
|
|
|
|
|
return body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_email(emailto, title, content):
|
|
|
|
|
"""
|
|
|
|
|
发送邮件(通过信号机制触发,解耦发送逻辑)
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
emailto: 收件人列表(如 ['user@example.com'])
|
|
|
|
|
title: 邮件标题
|
|
|
|
|
content: 邮件内容(HTML 格式)
|
|
|
|
|
"""
|
|
|
|
|
# 延迟导入信号:避免循环导入
|
|
|
|
|
from djangoblog.blog_signals import send_email_signal
|
|
|
|
|
# 发送信号,由信号接收器(如 send_email_signal_handler)处理实际发送
|
|
|
|
|
send_email_signal.send(
|
|
|
|
|
send_email.__class__,
|
|
|
|
|
send_email.__class__, # 信号发送者(此处用当前函数的类)
|
|
|
|
|
emailto=emailto,
|
|
|
|
|
title=title,
|
|
|
|
|
content=content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_code() -> str:
|
|
|
|
|
"""生成随机数验证码"""
|
|
|
|
|
"""
|
|
|
|
|
生成 6 位数字验证码(用于邮箱验证、登录验证码等)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: 6 位数字字符串
|
|
|
|
|
"""
|
|
|
|
|
# 从数字字符集中随机选择 6 个,拼接为字符串
|
|
|
|
|
return ''.join(random.sample(string.digits, 6))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_dict_to_url(dict):
|
|
|
|
|
from urllib.parse import quote
|
|
|
|
|
"""
|
|
|
|
|
将字典转换为 URL 参数字符串(如 {'a':1, 'b':2} → 'a=1&b=2')
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
dict: 键值对字典
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: URL 编码后的参数字符串
|
|
|
|
|
"""
|
|
|
|
|
from urllib.parse import quote # 延迟导入:避免启动依赖
|
|
|
|
|
# 对键和值进行 URL 编码(保留 '/' 不编码),再拼接为 "k=v&k2=v2" 格式
|
|
|
|
|
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
|
|
|
|
|
for k, v in dict.items()])
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_blog_setting():
|
|
|
|
|
"""
|
|
|
|
|
获取博客系统设置(如站点名称、描述等),优先从缓存获取
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
BlogSettings: 博客设置模型实例
|
|
|
|
|
"""
|
|
|
|
|
# 尝试从缓存获取
|
|
|
|
|
value = cache.get('get_blog_setting')
|
|
|
|
|
if value:
|
|
|
|
|
return value
|
|
|
|
|
else:
|
|
|
|
|
# 延迟导入模型:避免循环导入
|
|
|
|
|
from blog.models import BlogSettings
|
|
|
|
|
# 若数据库中无设置记录,初始化默认设置
|
|
|
|
|
if not BlogSettings.objects.count():
|
|
|
|
|
setting = BlogSettings()
|
|
|
|
|
setting.site_name = 'djangoblog'
|
|
|
|
|
setting.site_description = '基于Django的博客系统'
|
|
|
|
|
setting.site_seo_description = '基于Django的博客系统'
|
|
|
|
|
setting.site_keywords = 'Django,Python'
|
|
|
|
|
setting.article_sub_length = 300
|
|
|
|
|
setting.sidebar_article_count = 10
|
|
|
|
|
setting.sidebar_comment_count = 5
|
|
|
|
|
setting.show_google_adsense = False
|
|
|
|
|
setting.open_site_comment = True
|
|
|
|
|
setting.analytics_code = ''
|
|
|
|
|
setting.beian_code = ''
|
|
|
|
|
setting.show_gongan_code = False
|
|
|
|
|
setting.comment_need_review = False
|
|
|
|
|
setting.save()
|
|
|
|
|
setting.site_name = 'djangoblog' # 站点名称
|
|
|
|
|
setting.site_description = '基于Django的博客系统' # 站点描述
|
|
|
|
|
setting.site_seo_description = '基于Django的博客系统' # SEO 描述
|
|
|
|
|
setting.site_keywords = 'Django,Python' # SEO 关键词
|
|
|
|
|
setting.article_sub_length = 300 # 文章摘要长度
|
|
|
|
|
setting.sidebar_article_count = 10 # 侧边栏显示文章数
|
|
|
|
|
setting.sidebar_comment_count = 5 # 侧边栏显示评论数
|
|
|
|
|
setting.show_google_adsense = False # 是否显示谷歌广告
|
|
|
|
|
setting.open_site_comment = True # 是否开启评论功能
|
|
|
|
|
setting.analytics_code = '' # 统计代码(如百度统计)
|
|
|
|
|
setting.beian_code = '' # 备案号
|
|
|
|
|
setting.show_gongan_code = False # 是否显示公安备案
|
|
|
|
|
setting.comment_need_review = False # 评论是否需要审核
|
|
|
|
|
setting.save() # 保存默认设置
|
|
|
|
|
# 从数据库获取设置
|
|
|
|
|
value = BlogSettings.objects.first()
|
|
|
|
|
logger.info('set cache get_blog_setting')
|
|
|
|
|
# 缓存设置(默认使用 cache_decorator 的有效期,或依赖全局缓存配置)
|
|
|
|
|
cache.set('get_blog_setting', value)
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_user_avatar(url):
|
|
|
|
|
'''
|
|
|
|
|
保存用户头像
|
|
|
|
|
:param url:头像url
|
|
|
|
|
:return: 本地路径
|
|
|
|
|
下载并保存用户头像到本地(用于第三方登录时的头像同步)
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
url: 头像的网络 URL
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: 本地头像的静态文件 URL(如 '/static/avatar/xxx.jpg')
|
|
|
|
|
'''
|
|
|
|
|
logger.info(url)
|
|
|
|
|
logger.info(url) # 记录头像 URL
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 本地头像存储目录(静态文件目录下的 avatar 文件夹)
|
|
|
|
|
basedir = os.path.join(settings.STATICFILES, 'avatar')
|
|
|
|
|
# 发送 HTTP 请求下载头像(超时 2 秒)
|
|
|
|
|
rsp = requests.get(url, timeout=2)
|
|
|
|
|
if rsp.status_code == 200:
|
|
|
|
|
if rsp.status_code == 200: # 下载成功
|
|
|
|
|
# 若目录不存在则创建
|
|
|
|
|
if not os.path.exists(basedir):
|
|
|
|
|
os.makedirs(basedir)
|
|
|
|
|
|
|
|
|
|
# 验证 URL 是否为图片格式(通过文件扩展名判断)
|
|
|
|
|
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
|
|
|
|
|
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
|
|
|
|
|
# 提取文件扩展名,默认为 .jpg
|
|
|
|
|
ext = os.path.splitext(url)[1] if isimage else '.jpg'
|
|
|
|
|
# 生成唯一文件名(UUID 避免冲突)
|
|
|
|
|
save_filename = str(uuid.uuid4().hex) + ext
|
|
|
|
|
logger.info('保存用户头像:' + basedir + save_filename)
|
|
|
|
|
# 写入文件到本地目录
|
|
|
|
|
with open(os.path.join(basedir, save_filename), 'wb+') as file:
|
|
|
|
|
file.write(rsp.content)
|
|
|
|
|
# 返回本地头像的静态 URL
|
|
|
|
|
return static('avatar/' + save_filename)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# 下载失败(如网络错误、超时),记录错误并返回默认头像
|
|
|
|
|
logger.error(e)
|
|
|
|
|
return static('blog/img/avatar.png')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_sidebar_cache():
|
|
|
|
|
from blog.models import LinkShowType
|
|
|
|
|
"""
|
|
|
|
|
删除侧边栏相关缓存(当侧边栏内容更新时调用,如新增文章、评论)
|
|
|
|
|
"""
|
|
|
|
|
from blog.models import LinkShowType # 延迟导入:避免循环依赖
|
|
|
|
|
# 侧边栏缓存键格式为 "sidebar + 链接类型值"(如 sidebar0、sidebar1)
|
|
|
|
|
keys = ["sidebar" + x for x in LinkShowType.values]
|
|
|
|
|
# 遍历删除所有侧边栏缓存键
|
|
|
|
|
for k in keys:
|
|
|
|
|
logger.info('delete sidebar key:' + k)
|
|
|
|
|
cache.delete(k)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_view_cache(prefix, keys):
|
|
|
|
|
from django.core.cache.utils import make_template_fragment_key
|
|
|
|
|
"""
|
|
|
|
|
删除指定模板片段的缓存(用于模板中用 {% cache %} 标签缓存的内容)
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
prefix: 缓存前缀(与模板中 {% cache %} 标签的前缀一致)
|
|
|
|
|
keys: 缓存键的参数列表(与模板中 {% cache %} 标签的参数一致)
|
|
|
|
|
"""
|
|
|
|
|
from django.core.cache.utils import make_template_fragment_key # 生成模板缓存键的工具
|
|
|
|
|
# 生成模板片段的缓存键
|
|
|
|
|
key = make_template_fragment_key(prefix, keys)
|
|
|
|
|
# 删除缓存
|
|
|
|
|
cache.delete(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_resource_url():
|
|
|
|
|
"""
|
|
|
|
|
获取静态资源的基础 URL(用于动态生成资源路径)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: 静态资源 URL 前缀(如 'http://example.com/static/')
|
|
|
|
|
"""
|
|
|
|
|
if settings.STATIC_URL:
|
|
|
|
|
return settings.STATIC_URL
|
|
|
|
|
else:
|
|
|
|
|
# 若未配置 STATIC_URL,从当前站点域名生成
|
|
|
|
|
site = get_current_site()
|
|
|
|
|
return 'http://' + site.domain + '/static/'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# HTML 清理配置:允许的标签和属性(防止 XSS 攻击)
|
|
|
|
|
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
|
|
|
|
|
'h2', 'p']
|
|
|
|
|
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
|
|
|
|
|
'h2', 'p'] # 允许的 HTML 标签
|
|
|
|
|
ALLOWED_ATTRIBUTES = { # 允许的标签属性(键为标签,值为属性列表)
|
|
|
|
|
'a': ['href', 'title'],
|
|
|
|
|
'abbr': ['title'],
|
|
|
|
|
'acronym': ['title']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_html(html):
|
|
|
|
|
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
|
|
|
|
|
"""
|
|
|
|
|
清理 HTML 内容,仅保留允许的标签和属性(防 XSS 攻击)
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
html: 原始 HTML 字符串
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: 清理后的安全 HTML 字符串
|
|
|
|
|
"""
|
|
|
|
|
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
|