|
|
|
@ -1,6 +1,6 @@
|
|
|
|
#!/usr/bin/env python
|
|
|
|
#!/usr/bin/env python
|
|
|
|
# encoding: utf-8
|
|
|
|
# encoding: utf-8
|
|
|
|
|
|
|
|
#xy: Django博客工具函数模块
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
@ -21,17 +21,20 @@ logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_max_articleid_commentid():
|
|
|
|
def get_max_articleid_commentid():
|
|
|
|
|
|
|
|
"""#xy: 获取最大文章ID和评论ID"""
|
|
|
|
from blog.models import Article
|
|
|
|
from blog.models import Article
|
|
|
|
from comments.models import Comment
|
|
|
|
from comments.models import Comment
|
|
|
|
return (Article.objects.latest().pk, Comment.objects.latest().pk)
|
|
|
|
return (Article.objects.latest().pk, Comment.objects.latest().pk)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_sha256(str):
|
|
|
|
def get_sha256(str):
|
|
|
|
|
|
|
|
"""#xy: 计算字符串的SHA256哈希值"""
|
|
|
|
m = sha256(str.encode('utf-8'))
|
|
|
|
m = sha256(str.encode('utf-8'))
|
|
|
|
return m.hexdigest()
|
|
|
|
return m.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cache_decorator(expiration=3 * 60):
|
|
|
|
def cache_decorator(expiration=3 * 60):
|
|
|
|
|
|
|
|
"""#xy: 缓存装饰器"""
|
|
|
|
def wrapper(func):
|
|
|
|
def wrapper(func):
|
|
|
|
def news(*args, **kwargs):
|
|
|
|
def news(*args, **kwargs):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
@ -41,12 +44,11 @@ def cache_decorator(expiration=3 * 60):
|
|
|
|
key = None
|
|
|
|
key = None
|
|
|
|
if not key:
|
|
|
|
if not key:
|
|
|
|
unique_str = repr((func, args, kwargs))
|
|
|
|
unique_str = repr((func, args, kwargs))
|
|
|
|
|
|
|
|
|
|
|
|
m = sha256(unique_str.encode('utf-8'))
|
|
|
|
m = sha256(unique_str.encode('utf-8'))
|
|
|
|
key = m.hexdigest()
|
|
|
|
key = m.hexdigest()
|
|
|
|
value = cache.get(key)
|
|
|
|
value = cache.get(key)
|
|
|
|
if value is not None:
|
|
|
|
if value is not None:
|
|
|
|
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
|
|
|
|
#xy: 缓存命中
|
|
|
|
if str(value) == '__default_cache_value__':
|
|
|
|
if str(value) == '__default_cache_value__':
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
@ -68,14 +70,7 @@ def cache_decorator(expiration=3 * 60):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def expire_view_cache(path, servername, serverport, key_prefix=None):
|
|
|
|
def expire_view_cache(path, servername, serverport, key_prefix=None):
|
|
|
|
'''
|
|
|
|
'''#xy: 刷新视图缓存'''
|
|
|
|
刷新视图缓存
|
|
|
|
|
|
|
|
:param path:url路径
|
|
|
|
|
|
|
|
:param servername:host
|
|
|
|
|
|
|
|
:param serverport:端口
|
|
|
|
|
|
|
|
:param key_prefix:前缀
|
|
|
|
|
|
|
|
:return:是否成功
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
from django.http import HttpRequest
|
|
|
|
from django.http import HttpRequest
|
|
|
|
from django.utils.cache import get_cache_key
|
|
|
|
from django.utils.cache import get_cache_key
|
|
|
|
|
|
|
|
|
|
|
|
@ -94,13 +89,16 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
|
|
|
|
|
|
|
|
|
|
|
|
@cache_decorator()
|
|
|
|
@cache_decorator()
|
|
|
|
def get_current_site():
|
|
|
|
def get_current_site():
|
|
|
|
|
|
|
|
"""#xy: 获取当前站点"""
|
|
|
|
site = Site.objects.get_current()
|
|
|
|
site = Site.objects.get_current()
|
|
|
|
return site
|
|
|
|
return site
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CommonMarkdown:
|
|
|
|
class CommonMarkdown:
|
|
|
|
|
|
|
|
"""#xy: Markdown处理工具类"""
|
|
|
|
@staticmethod
|
|
|
|
@staticmethod
|
|
|
|
def _convert_markdown(value):
|
|
|
|
def _convert_markdown(value):
|
|
|
|
|
|
|
|
"""#xy: 转换Markdown文本"""
|
|
|
|
md = markdown.Markdown(
|
|
|
|
md = markdown.Markdown(
|
|
|
|
extensions=[
|
|
|
|
extensions=[
|
|
|
|
'extra',
|
|
|
|
'extra',
|
|
|
|
@ -115,16 +113,19 @@ class CommonMarkdown:
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
@staticmethod
|
|
|
|
def get_markdown_with_toc(value):
|
|
|
|
def get_markdown_with_toc(value):
|
|
|
|
|
|
|
|
"""#xy: 获取带目录的Markdown"""
|
|
|
|
body, toc = CommonMarkdown._convert_markdown(value)
|
|
|
|
body, toc = CommonMarkdown._convert_markdown(value)
|
|
|
|
return body, toc
|
|
|
|
return body, toc
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
@staticmethod
|
|
|
|
def get_markdown(value):
|
|
|
|
def get_markdown(value):
|
|
|
|
|
|
|
|
"""#xy: 获取Markdown内容"""
|
|
|
|
body, toc = CommonMarkdown._convert_markdown(value)
|
|
|
|
body, toc = CommonMarkdown._convert_markdown(value)
|
|
|
|
return body
|
|
|
|
return body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_email(emailto, title, content):
|
|
|
|
def send_email(emailto, title, content):
|
|
|
|
|
|
|
|
"""#xy: 发送邮件"""
|
|
|
|
from djangoblog.blog_signals import send_email_signal
|
|
|
|
from djangoblog.blog_signals import send_email_signal
|
|
|
|
send_email_signal.send(
|
|
|
|
send_email_signal.send(
|
|
|
|
send_email.__class__,
|
|
|
|
send_email.__class__,
|
|
|
|
@ -134,11 +135,12 @@ def send_email(emailto, title, content):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_code() -> str:
|
|
|
|
def generate_code() -> str:
|
|
|
|
"""生成随机数验证码"""
|
|
|
|
"""#xy: 生成随机数验证码"""
|
|
|
|
return ''.join(random.sample(string.digits, 6))
|
|
|
|
return ''.join(random.sample(string.digits, 6))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_dict_to_url(dict):
|
|
|
|
def parse_dict_to_url(dict):
|
|
|
|
|
|
|
|
"""#xy: 解析字典为URL参数"""
|
|
|
|
from urllib.parse import quote
|
|
|
|
from urllib.parse import quote
|
|
|
|
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
|
|
|
|
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
|
|
|
|
for k, v in dict.items()])
|
|
|
|
for k, v in dict.items()])
|
|
|
|
@ -146,12 +148,14 @@ def parse_dict_to_url(dict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_blog_setting():
|
|
|
|
def get_blog_setting():
|
|
|
|
|
|
|
|
"""#xy: 获取博客设置"""
|
|
|
|
value = cache.get('get_blog_setting')
|
|
|
|
value = cache.get('get_blog_setting')
|
|
|
|
if value:
|
|
|
|
if value:
|
|
|
|
return value
|
|
|
|
return value
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
from blog.models import BlogSettings
|
|
|
|
from blog.models import BlogSettings
|
|
|
|
if not BlogSettings.objects.count():
|
|
|
|
if not BlogSettings.objects.count():
|
|
|
|
|
|
|
|
#xy: 创建默认博客设置
|
|
|
|
setting = BlogSettings()
|
|
|
|
setting = BlogSettings()
|
|
|
|
setting.site_name = 'djangoblog'
|
|
|
|
setting.site_name = 'djangoblog'
|
|
|
|
setting.site_description = '基于Django的博客系统'
|
|
|
|
setting.site_description = '基于Django的博客系统'
|
|
|
|
@ -174,15 +178,12 @@ def get_blog_setting():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_user_avatar(url):
|
|
|
|
def save_user_avatar(url):
|
|
|
|
'''
|
|
|
|
'''#xy: 保存用户头像'''
|
|
|
|
保存用户头像
|
|
|
|
logger.info(f"保存用户头像: {url}")
|
|
|
|
:param url:头像url
|
|
|
|
|
|
|
|
:return: 本地路径
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
logger.info(url)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
basedir = os.path.join(settings.STATICFILES, 'avatar')
|
|
|
|
basedir = os.path.join(settings.STATICFILES, 'avatar')
|
|
|
|
|
|
|
|
#xy: 下载头像
|
|
|
|
rsp = requests.get(url, timeout=2)
|
|
|
|
rsp = requests.get(url, timeout=2)
|
|
|
|
if rsp.status_code == 200:
|
|
|
|
if rsp.status_code == 200:
|
|
|
|
if not os.path.exists(basedir):
|
|
|
|
if not os.path.exists(basedir):
|
|
|
|
@ -197,11 +198,12 @@ def save_user_avatar(url):
|
|
|
|
file.write(rsp.content)
|
|
|
|
file.write(rsp.content)
|
|
|
|
return static('avatar/' + save_filename)
|
|
|
|
return static('avatar/' + save_filename)
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(e)
|
|
|
|
logger.error(f"保存用户头像失败: {e}")
|
|
|
|
return static('blog/img/avatar.png')
|
|
|
|
return static('blog/img/avatar.png')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_sidebar_cache():
|
|
|
|
def delete_sidebar_cache():
|
|
|
|
|
|
|
|
"""#xy: 删除侧边栏缓存"""
|
|
|
|
from blog.models import LinkShowType
|
|
|
|
from blog.models import LinkShowType
|
|
|
|
keys = ["sidebar" + x for x in LinkShowType.values]
|
|
|
|
keys = ["sidebar" + x for x in LinkShowType.values]
|
|
|
|
for k in keys:
|
|
|
|
for k in keys:
|
|
|
|
@ -210,12 +212,14 @@ def delete_sidebar_cache():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_view_cache(prefix, keys):
|
|
|
|
def delete_view_cache(prefix, keys):
|
|
|
|
|
|
|
|
"""#xy: 删除视图缓存"""
|
|
|
|
from django.core.cache.utils import make_template_fragment_key
|
|
|
|
from django.core.cache.utils import make_template_fragment_key
|
|
|
|
key = make_template_fragment_key(prefix, keys)
|
|
|
|
key = make_template_fragment_key(prefix, keys)
|
|
|
|
cache.delete(key)
|
|
|
|
cache.delete(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_resource_url():
|
|
|
|
def get_resource_url():
|
|
|
|
|
|
|
|
"""#xy: 获取资源URL"""
|
|
|
|
if settings.STATIC_URL:
|
|
|
|
if settings.STATIC_URL:
|
|
|
|
return settings.STATIC_URL
|
|
|
|
return settings.STATIC_URL
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
@ -223,10 +227,11 @@ def get_resource_url():
|
|
|
|
return 'http://' + site.domain + '/static/'
|
|
|
|
return 'http://' + site.domain + '/static/'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#xy: HTML清理配置
|
|
|
|
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
|
|
|
|
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
|
|
|
|
'h2', 'p', 'span', 'div']
|
|
|
|
'h2', 'p', 'span', 'div']
|
|
|
|
|
|
|
|
|
|
|
|
# 安全的class值白名单 - 只允许代码高亮相关的class
|
|
|
|
#xy: 安全的class值白名单 - 只允许代码高亮相关的class
|
|
|
|
ALLOWED_CLASSES = [
|
|
|
|
ALLOWED_CLASSES = [
|
|
|
|
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
|
|
|
|
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
|
|
|
|
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
|
|
|
|
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
|
|
|
|
@ -236,14 +241,14 @@ ALLOWED_CLASSES = [
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def class_filter(tag, name, value):
|
|
|
|
def class_filter(tag, name, value):
|
|
|
|
"""自定义class属性过滤器"""
|
|
|
|
"""#xy: 自定义class属性过滤器"""
|
|
|
|
if name == 'class':
|
|
|
|
if name == 'class':
|
|
|
|
# 只允许预定义的安全class值
|
|
|
|
#xy: 只允许预定义的安全class值
|
|
|
|
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
|
|
|
|
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
|
|
|
|
return ' '.join(allowed_classes) if allowed_classes else False
|
|
|
|
return ' '.join(allowed_classes) if allowed_classes else False
|
|
|
|
return value
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
# 安全的属性白名单
|
|
|
|
#xy: 安全的属性白名单
|
|
|
|
ALLOWED_ATTRIBUTES = {
|
|
|
|
ALLOWED_ATTRIBUTES = {
|
|
|
|
'a': ['href', 'title'],
|
|
|
|
'a': ['href', 'title'],
|
|
|
|
'abbr': ['title'],
|
|
|
|
'abbr': ['title'],
|
|
|
|
@ -254,19 +259,19 @@ ALLOWED_ATTRIBUTES = {
|
|
|
|
'code': class_filter
|
|
|
|
'code': class_filter
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 安全的协议白名单 - 防止javascript:等危险协议
|
|
|
|
#xy: 安全的协议白名单 - 防止javascript:等危险协议
|
|
|
|
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
|
|
|
|
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_html(html):
|
|
|
|
def sanitize_html(html):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
安全的HTML清理函数
|
|
|
|
#xy: 安全的HTML清理函数
|
|
|
|
使用bleach库进行白名单过滤,防止XSS攻击
|
|
|
|
#xy: 使用bleach库进行白名单过滤,防止XSS攻击
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
return bleach.clean(
|
|
|
|
return bleach.clean(
|
|
|
|
html,
|
|
|
|
html,
|
|
|
|
tags=ALLOWED_TAGS,
|
|
|
|
tags=ALLOWED_TAGS,
|
|
|
|
attributes=ALLOWED_ATTRIBUTES,
|
|
|
|
attributes=ALLOWED_ATTRIBUTES,
|
|
|
|
protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
|
|
|
|
protocols=ALLOWED_PROTOCOLS, #xy: 限制允许的协议
|
|
|
|
strip=True, # 移除不允许的标签而不是转义
|
|
|
|
strip=True, #xy: 移除不允许的标签而不是转义
|
|
|
|
strip_comments=True # 移除HTML注释
|
|
|
|
strip_comments=True #xy: 移除HTML注释
|
|
|
|
)
|
|
|
|
)
|