姜雨菲代码注释

master
zyl 3 months ago
parent b4dd4c2c92
commit 7f3152ac7b

@ -0,0 +1,8 @@
# Django应用配置指定模块
# 该模块的主要功能是定义当前Django应用的默认配置类
# 当Django加载应用时会根据此配置类进行应用的初始化设置
# 包括应用名称、信号注册、权限配置等应用级别的配置项
# 指定当前Django应用的默认配置类为'djangoblog.apps.DjangoblogAppConfig'
# Django在启动时会自动加载该配置类执行其中的初始化逻辑如ready()方法)
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -0,0 +1,89 @@
# Django博客系统的Admin配置模块
# 该模块用于自定义Django管理后台Admin Site包括管理员站点的属性设置、权限控制
# 以及注册系统中各模型到管理后台,实现对数据的可视化管理
# 导入Django内置的AdminSite及相关模型、管理类
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
# 导入各应用的Admin配置和模型账号、博客、评论、OAuth等模块
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
"""
自定义的Django管理站点类继承自AdminSite
用于个性化管理后台的显示信息和权限控制
"""
# 管理后台页面顶部的标题
site_header = 'djangoblog administration'
# 浏览器标签页的标题
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""初始化方法,调用父类构造函数"""
super().__init__(name)
def has_permission(self, request):
"""
重写权限检查方法控制谁可以访问管理后台
仅允许超级用户is_superuser访问
"""
return request.user.is_superuser
# 以下为注释掉的自定义URL示例如需扩展管理后台URL可启用
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# return urls + my_urls
# 实例化自定义的管理站点
admin_site = DjangoBlogAdminSite(name='admin')
# 注册博客核心模型到管理站点关联对应的Admin配置类
admin_site.register(Article, ArticlelAdmin) # 文章模型
admin_site.register(Category, CategoryAdmin) # 分类模型
admin_site.register(Tag, TagAdmin) # 标签模型
admin_site.register(Links, LinksAdmin) # 链接模型
admin_site.register(SideBar, SideBarAdmin) # 侧边栏模型
admin_site.register(BlogSettings, BlogSettingsAdmin) # 博客设置模型
# 注册服务器管理相关模型
admin_site.register(commands, CommandsAdmin) # 命令模型
admin_site.register(EmailSendLog, EmailSendLogAdmin) # 邮件发送日志模型
# 注册用户模型
admin_site.register(BlogUser, BlogUserAdmin) # 自定义用户模型
# 注册评论模型
admin_site.register(Comment, CommentAdmin) # 评论模型
# 注册OAuth相关模型
admin_site.register(OAuthUser, OAuthUserAdmin) # OAuth用户模型
admin_site.register(OAuthConfig, OAuthConfigAdmin) # OAuth配置模型
# 注册位置追踪相关模型
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # 位置日志模型
# 注册站点和日志模型Django内置
admin_site.register(Site, SiteAdmin) # 站点模型
admin_site.register(LogEntry, LogEntryAdmin) # 操作日志模型

@ -0,0 +1,29 @@
# Django博客应用配置类模块
# 该模块定义了Django博客应用djangoblog的配置类用于设置应用的核心属性和初始化逻辑
# 主要功能包括:指定默认的自增字段类型、定义应用名称、以及在应用就绪时加载插件
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
"""
Django博客应用的配置类继承自Django的AppConfig
用于配置应用的元数据和生命周期钩子
"""
# 指定模型默认的自增主键字段类型为BigAutoField支持更大范围的整数
default_auto_field = 'django.db.models.BigAutoField'
# 应用的名称,对应项目中的应用目录名
name = 'djangoblog'
def ready(self):
"""
应用就绪时执行的方法Django生命周期钩子
当应用加载完成并准备好处理请求时调用通常用于初始化操作
"""
# 调用父类的ready()方法,确保基类的初始化逻辑执行
super().ready()
# 导入并加载插件:在应用就绪后加载所有激活的插件
# 从当前应用的plugin_manage.loader模块导入load_plugins函数
from .plugin_manage.loader import load_plugins
# 执行插件加载函数,完成插件的动态导入和初始化
load_plugins()

@ -0,0 +1,177 @@
# Django博客系统信号处理模块
# 该模块用于注册和处理Django内置信号及自定义信号实现事件驱动的业务逻辑
# 核心功能包括邮件发送、OAuth用户登录处理、模型保存后缓存清理/搜索引擎通知、用户登录登出缓存处理等
# 通过信号机制解耦业务逻辑,当特定事件触发时自动执行对应处理函数
import _thread
import logging
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
# 导入项目内部模块评论相关、插件通知、缓存工具、站点工具、OAuth模型
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
# 初始化日志记录器,记录信号处理过程中的信息和错误
logger = logging.getLogger(__name__)
# 自定义信号OAuth用户登录信号携带参数'id'OAuthUser的主键
oauth_user_login_signal = django.dispatch.Signal(['id'])
# 自定义信号:邮件发送信号,携带参数'emailto'(收件人列表)、'title'(邮件标题)、'content'(邮件内容)
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""
邮件发送信号的处理函数
当send_email_signal信号触发时自动发送HTML格式邮件并记录发送日志
"""
# 从信号参数中提取邮件相关信息
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
# 构建HTML格式邮件content_subtype设为html支持富文本
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL, # 发件人从Django配置中读取
to=emailto) # 收件人列表
msg.content_subtype = "html" # 指定邮件内容为HTML格式
# 初始化邮件发送日志模型,记录发送详情
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto) # 多个收件人用逗号拼接存储
try:
# 发送邮件result为成功发送的邮件数量
result = msg.send()
log.send_result = result > 0 # 发送数量>0表示发送成功
except Exception as e:
# 捕获发送异常,记录错误日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False # 标记发送失败
log.save() # 保存日志到数据库
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""
OAuth用户登录信号的处理函数
当OAuth用户登录成功后处理用户头像如跨域头像本地化存储并清理侧边栏缓存
"""
# 从信号参数中提取OAuthUser的id查询对应的用户实例
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
# 获取当前站点域名(用于判断头像是否为本地地址)
site = get_current_site().domain
# 若用户有头像且头像地址不包含当前站点域名(跨域头像),则本地化存储
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture) # 保存头像到本地并更新地址
oauthuser.save() # 保存更新后的用户信息
# 清理侧边栏缓存(确保登录后侧边栏展示最新数据)
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
"""
Django模型保存后信号的处理函数post_save
触发时机任何模型执行save()方法后新增/更新
主要处理搜索引擎通知缓存清理评论审核通过后的联动操作
"""
clearcache = False # 标记是否需要清理全局缓存
# 跳过Admin操作日志模型LogEntry的处理无需触发后续逻辑
if isinstance(instance, LogEntry):
return
# 若模型实例有get_full_url方法通常是博客文章等需要对外展示的模型
if 'get_full_url' in dir(instance):
# 判断是否仅更新浏览量字段views
is_update_views = update_fields == {'views'}
# 非测试环境且不是仅更新浏览量时,通知搜索引擎(如百度)收录新链接
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url() # 获取模型实例的完整访问链接
SpiderNotify.baidu_notify([notify_url]) # 通知百度搜索引擎
except Exception as ex:
logger.error("notify sipder", ex) # 记录搜索引擎通知失败的错误
# 非浏览量更新时,标记需要清理缓存
if not is_update_views:
clearcache = True
# 若保存的是评论模型实例
if isinstance(instance, Comment):
# 仅处理审核通过的评论is_enable为True
if instance.is_enable:
# 获取评论对应的文章绝对路径
path = instance.article.get_absolute_url()
site = get_current_site().domain
# 处理带端口的域名如localhost:8000仅保留主域名部分
if site.find(':') > 0:
site = site[0:site.find(':')]
# 清理文章详情页的视图缓存(确保评论实时展示)
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
# 清理SEO相关缓存
if cache.get('seo_processor'):
cache.delete('seo_processor')
# 清理该文章的评论列表缓存
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
# 清理侧边栏缓存和评论相关视图缓存
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
# 启动新线程发送评论通知邮件(避免阻塞主线程)
_thread.start_new_thread(send_comment_email, (instance,))
# 若标记需要清理缓存,则执行全局缓存清理
if clearcache:
cache.clear()
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
"""
用户登录/登出信号的处理函数
触发时机用户登录user_logged_in或登出user_logged_out
主要处理记录日志并清理侧边栏缓存确保登录状态变化后展示最新数据
"""
# 若用户存在且用户名有效
if user and user.username:
logger.info(user) # 记录用户登录/登出日志
delete_sidebar_cache() # 清理侧边栏缓存
# cache.clear() # 全局缓存清理(按需启用)

@ -0,0 +1,183 @@
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
for m in models:
m.delete()
return True
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"])
else:
keywords.append(suggest["text"])
return ' '.join(keywords)
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
**additional_fields)
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
def get_count(self):
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -0,0 +1,72 @@
#姜雨菲导入Django用户模型获取工具
from django.contrib.auth import get_user_model
#姜雨菲: 导入Django的Feed基类用于创建RSS/Atom订阅源
from django.contrib.syndication.views import Feed
#姜雨菲: 导入时区处理工具
from django.utils import timezone
#姜雨菲: 导入RSS 2.0版本的生成器
from django.utils.feedgenerator import Rss201rev2Feed
#姜雨菲: 导入博客文章模型
from blog.models import Article
#姜雨菲: 导入自定义的Markdown处理工具
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
"""博客网站的RSS订阅源类继承自Django的Feed基类"""
#姜雨菲: 指定订阅源类型为RSS 2.0版本
feed_type = Rss201rev2Feed
# 订阅源的描述信息
description = '大巧无工,重剑无锋.'
# 订阅源的标题
title = "且听风吟 大巧无工,重剑无锋. "
# 订阅源的链接(相对路径)
link = "/feed/"
def author_name(self):
"""返回订阅源作者名称"""
# 获取第一个用户的昵称作为作者名
return get_user_model().objects.first().nickname
def author_link(self):
"""返回订阅源作者的链接"""
# 获取第一个用户的绝对URL
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""
定义订阅源包含的项目列表
返回最新发布的5篇文章
"""
# 筛选类型为'article'(a)且状态为'published'(p)的文章
# 按发布时间倒序排列取前5篇
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""返回单个项目(文章)的标题"""
return item.title
def item_description(self, item):
"""返回单个项目(文章)的描述"""
# 将文章正文从Markdown格式转换为HTML
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
"""返回订阅源的版权信息"""
# 获取当前时间,并格式化版权信息
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""返回单个项目(文章)的链接"""
return item.get_absolute_url()
def item_guid(self, item):
"""
返回单个项目的唯一标识符(guid)
此处返回空值实际应用中通常应返回唯一标识如文章ID等
"""
return

@ -0,0 +1,136 @@
#姜雨菲: 导入Django管理后台核心模块
from django.contrib import admin
#姜雨菲: 导入日志相关常量和模型
from django.contrib.admin.models import DELETION # 表示"删除"操作的常量
from django.contrib.contenttypes.models import ContentType # 内容类型模型,用于关联不同模型
#姜雨菲: 导入URL反向解析和异常处理
from django.urls import reverse, NoReverseMatch
# 导入字符串处理工具
from django.utils.encoding import force_str
# 导入HTML转义工具
from django.utils.html import escape
# 导入安全字符串标记工具用于渲染HTML
from django.utils.safestring import mark_safe
# 导入国际化翻译工具
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
"""
自定义管理员日志(LogEntry)的管理类
用于在Django admin后台展示和管理系统操作日志
"""
# 列表页的筛选器:按内容类型筛选
list_filter = [
'content_type'
]
# 搜索字段:支持按对象表示和变更消息搜索
search_fields = [
'object_repr', # 对象的字符串表示
'change_message' # 操作变更的描述信息
]
# 列表页中可点击的链接字段
list_display_links = [
'action_time', # 操作时间
'get_change_message', # 变更消息
]
# 列表页展示的字段
list_display = [
'action_time', # 操作时间
'user_link', # 操作用户(带链接)
'content_type', # 操作的内容类型(模型)
'object_link', # 操作的对象(带链接)
'get_change_message', # 变更消息
]
def has_add_permission(self, request):
"""禁用添加权限:不允许手动添加日志记录"""
return False
def has_change_permission(self, request, obj=None):
"""
限制修改权限
- 仅超级用户或拥有change_logentry权限的用户可查看
- 禁止POST请求即不允许修改日志内容
"""
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
"""禁用删除权限:不允许删除日志记录"""
return False
def object_link(self, obj):
"""
生成操作对象的链接若对象存在
对于已删除的对象仅显示文本对于存在的对象显示可点击的链接
"""
# 先对对象的字符串表示进行HTML转义防止XSS攻击
object_link = escape(obj.object_repr)
# 获取操作对象的内容类型
content_type = obj.content_type
# 如果不是删除操作且内容类型存在,尝试生成编辑链接
if obj.action_flag != DELETION and content_type is not None:
try:
# 反向解析对象的编辑页面URL
url = reverse(
# 生成admin的URL名称格式app_label_model_change
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id] # 传递对象ID作为参数
)
# 生成带链接的HTML
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
# 若无法解析URL如模型未注册到admin则只显示文本
pass
# 标记为安全字符串允许Django渲染HTML
return mark_safe(object_link)
# 配置列表页字段的排序和显示名称
object_link.admin_order_field = 'object_repr' # 允许按对象表示排序
object_link.short_description = _('object') # 列表页显示的列名(支持国际化)
def user_link(self, obj):
"""生成操作用户的链接(指向用户编辑页面)"""
# 获取用户模型的内容类型
content_type = ContentType.objects.get_for_model(type(obj.user))
# 对用户名进行HTML转义
user_link = escape(force_str(obj.user))
try:
# 反向解析用户编辑页面的URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk] # 传递用户ID作为参数
)
# 生成带链接的HTML
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
# 若无法解析URL只显示用户名
pass
# 标记为安全字符串允许渲染HTML
return mark_safe(user_link)
# 配置用户链接字段的排序和显示名称
user_link.admin_order_field = 'user' # 允许按用户排序
user_link.short_description = _('user') # 列表页显示的列名(支持国际化)
def get_queryset(self, request):
"""优化查询集预加载content_type减少数据库查询次数"""
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type') # 使用预加载优化性能
def get_actions(self, request):
"""移除批量删除操作:不允许批量删除日志"""
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

@ -0,0 +1,247 @@
import logging # 用于日志记录
from pathlib import Path # 用于文件路径处理
from django.template import TemplateDoesNotExist # Django模板不存在异常
from django.template.loader import render_to_string # 用于渲染模板为字符串
# 创建日志记录器,用于记录插件相关日志
logger = logging.getLogger(__name__)
class BasePlugin:
"""
插件基类所有自定义插件需继承此类并实现特定方法
提供插件元数据管理位置渲染模板渲染静态资源处理等基础功能
"""
# 插件元数据(子类必须重写这些属性)
PLUGIN_NAME = None # 插件名称(如"天气插件"
PLUGIN_DESCRIPTION = None # 插件描述(功能说明)
PLUGIN_VERSION = None # 插件版本(如"1.0.0"
PLUGIN_AUTHOR = None # 插件作者
# 插件配置(子类可根据需求重写)
SUPPORTED_POSITIONS = [] # 支持的显示位置(如['sidebar', 'footer']表示支持侧边栏和页脚)
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高,用于多个插件在同一位置排序)
POSITION_PRIORITIES = {} # 各位置的优先级(覆盖默认值,如{'sidebar': 50}表示侧边栏优先级为50
def __init__(self):
"""初始化插件,验证元数据并设置基础属性"""
# 校验必须的元数据是否完整,不完整则抛出异常
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("插件元数据PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION必须定义。")
# 设置插件路径和唯一标识
self.plugin_dir = self._get_plugin_directory() # 插件所在目录路径
self.plugin_slug = self._get_plugin_slug() # 插件唯一标识(默认使用目录名)
# 初始化插件并注册钩子
self.init_plugin()
self.register_hooks()
def _get_plugin_directory(self):
"""获取插件所在的目录路径(内部方法)"""
import inspect
# 通过inspect模块获取当前类的定义文件路径再获取其所在目录
plugin_file = inspect.getfile(self.__class__)
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""获取插件的唯一标识符(默认使用插件目录名,内部方法)"""
return self.plugin_dir.name
def init_plugin(self):
"""
插件初始化逻辑钩子方法
子类可重写此方法实现自定义初始化操作如加载配置连接数据库等
"""
logger.info(f'{self.PLUGIN_NAME} 初始化完成。')
def register_hooks(self):
"""
注册插件钩子钩子方法
子类可重写此方法注册自定义钩子如响应Django信号注册URL路由等
"""
pass
# === 位置渲染系统 ===
def render_position_widget(self, position, context, **kwargs):
"""
根据指定位置渲染插件组件核心方法
Args:
position: 位置标识'sidebar'表示侧边栏
context: 模板上下文包含当前请求用户等信息
**kwargs: 额外参数如文章ID页面类型等
Returns:
dict: 包含渲染结果的字典格式为
{'html': 'HTML内容', 'priority': 优先级, 'plugin_name': 插件名}
若不支持该位置或不满足显示条件返回None
"""
# 检查当前位置是否在插件支持的位置列表中
if position not in self.SUPPORTED_POSITIONS:
return None
# 检查是否满足显示条件调用should_display方法
if not self.should_display(position, context, **kwargs):
return None
# 动态调用对应位置的渲染方法如position为'sidebar'则调用render_sidebar_widget
method_name = f'render_{position}_widget'
if hasattr(self, method_name):
# 调用具体位置的渲染方法获取HTML内容
html = getattr(self, method_name)(context, **kwargs)
if html: # 若渲染成功有HTML内容
# 确定当前位置的优先级优先使用POSITION_PRIORITIES否则用默认值
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
return None
def should_display(self, position, context, **kwargs):
"""
判断插件是否应该在指定位置显示钩子方法
子类可重写此方法实现条件显示逻辑如只在特定页面/用户组显示
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: True表示显示False表示不显示
"""
return True # 默认始终显示
# === 各位置渲染方法 - 子类需根据支持的位置重写 ===
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏组件(钩子方法),子类重写此方法实现侧边栏内容"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部组件(钩子方法),子类重写此方法实现文章底部内容"""
return None
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件(钩子方法),子类重写此方法实现文章顶部内容"""
return None
def render_header_widget(self, context, **kwargs):
"""渲染页头组件(钩子方法),子类重写此方法实现页头内容"""
return None
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件(钩子方法),子类重写此方法实现页脚内容"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件(钩子方法),子类重写此方法实现评论区前内容"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件(钩子方法),子类重写此方法实现评论区后内容"""
return None
# === 模板系统 ===
def render_template(self, template_name, context=None):
"""
渲染插件自带的模板文件
Args:
template_name: 模板文件名"sidebar.html"
context: 模板上下文传递给模板的变量
Returns:
str: 渲染后的HTML字符串模板不存在则返回空字符串
"""
if context is None:
context = {} # 默认为空上下文
# 构建模板路径plugins/插件标识/模板名遵循Django模板查找规则
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
# 调用Django的render_to_string渲染模板
return render_to_string(template_path, context)
except TemplateDoesNotExist:
# 模板不存在时记录警告日志
logger.warning(f"插件模板不存在:{template_path}")
return ""
# === 静态资源系统 ===
def get_static_url(self, static_file):
"""
获取插件静态文件的URL如CSSJS图片等
Args:
static_file: 静态文件相对路径"css/style.css"
Returns:
str: 静态文件的完整URL"/static/myplugin/static/myplugin/css/style.css"
"""
from django.templatetags.static import static # 导入Django的static标签
# 构建静态文件路径:插件标识/static/插件标识/文件路径遵循Django静态文件规则
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""
获取插件需要加载的CSS文件列表钩子方法
子类重写此方法返回CSS文件路径列表框架会自动在页面加载这些CSS
Returns:
list: CSS文件路径列表["css/style.css"]
"""
return []
def get_js_files(self):
"""
获取插件需要加载的JavaScript文件列表钩子方法
子类重写此方法返回JS文件路径列表框架会自动在页面加载这些JS
Returns:
list: JS文件路径列表["js/script.js"]
"""
return []
def get_head_html(self, context=None):
"""
获取需要插入到HTML头部<head>标签内的内容钩子方法
子类重写此方法返回自定义HTML如额外的CSS链接meta标签等
Returns:
str: 要插入<head>的HTML字符串
"""
return ""
def get_body_html(self, context=None):
"""
获取需要插入到HTML body底部的内容钩子方法
子类重写此方法返回自定义HTML如额外的JS脚本
Returns:
str: 要插入<body>底部的HTML字符串
"""
return ""
def get_plugin_info(self):
"""
获取插件的详细信息用于插件管理展示等
Returns:
dict: 包含插件元数据和配置的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION,
'author': self.PLUGIN_AUTHOR,
'slug': self.plugin_slug,
'directory': str(self.plugin_dir),
'supported_positions': self.SUPPORTED_POSITIONS,
'priorities': self.POSITION_PRIORITIES
}

@ -0,0 +1,35 @@
# 文章相关事件常量模块
# 该模块定义了与文章操作相关的事件常量、内容钩子常量、位置钩子常量以及资源注入钩子常量
# 这些常量用于在系统中统一标识不同的操作事件和钩子位置,便于模块间的交互和扩展
# 文章操作事件常量
# 用于标识文章详情加载事件,当加载文章详情时触发相关处理逻辑
ARTICLE_DETAIL_LOAD = 'article_detail_load'
# 用于标识文章创建事件,当创建新文章时触发相关处理逻辑
ARTICLE_CREATE = 'article_create'
# 用于标识文章更新事件,当更新已有文章时触发相关处理逻辑
ARTICLE_UPDATE = 'article_update'
# 用于标识文章删除事件,当删除文章时触发相关处理逻辑
ARTICLE_DELETE = 'article_delete'
# 文章内容钩子常量
# 定义文章内容处理的钩子名称,用于在文章内容渲染前后插入自定义处理逻辑
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 位置钩子常量字典
# 键为位置标识,值为对应的钩子名称,用于在页面不同位置挂载自定义组件或逻辑
POSITION_HOOKS = {
'article_top': 'article_top_widgets', # 文章顶部位置的钩子,用于挂载顶部组件
'article_bottom': 'article_bottom_widgets', # 文章底部位置的钩子,用于挂载底部组件
'sidebar': 'sidebar_widgets', # 侧边栏位置的钩子,用于挂载侧边栏组件
'header': 'header_widgets', # 页头位置的钩子,用于挂载页头组件
'footer': 'footer_widgets', # 页脚位置的钩子,用于挂载页脚组件
'comment_before': 'comment_before_widgets', # 评论区之前位置的钩子,用于在评论前插入内容
'comment_after': 'comment_after_widgets', # 评论区之后位置的钩子,用于在评论后插入内容
}
# 资源注入钩子常量
# 用于标识在HTML头部注入资源如CSS、JS的钩子可通过该钩子添加头部资源
HEAD_RESOURCES_HOOK = 'head_resources'
# 用于标识在HTML body部分注入资源如JS的钩子可通过该钩子添加body资源
BODY_RESOURCES_HOOK = 'body_resources'

@ -0,0 +1,89 @@
# 钩子系统核心模块
# 该模块实现了一个轻量级的钩子Hook机制支持注册回调函数、执行动作钩子Action Hook和过滤钩子Filter Hook
# 主要功能包括:管理钩子与回调函数的映射关系、按顺序执行钩子回调、处理回调执行过程中的异常并记录日志
# 适用于需要模块解耦、灵活扩展的场景,通过钩子机制实现不同组件间的间接交互
import logging
# 初始化日志记录器,用于记录钩子注册、执行过程中的调试信息和错误信息
logger = logging.getLogger(__name__)
# 私有字典,用于存储钩子名称与回调函数列表的映射关系
# 键为钩子名称(字符串),值为注册到该钩子的回调函数列表
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个回调函数到指定的钩子上
同一钩子可以注册多个回调函数执行时将按注册顺序依次调用
参数:
hook_name: 钩子名称用于标识一组相关的回调函数
callback: 可调用对象函数方法等将在钩子触发时执行
"""
# 如果钩子名称不在映射表中,初始化一个空列表用于存储回调
if hook_name not in _hooks:
_hooks[hook_name] = []
# 将回调函数添加到对应钩子的列表中
_hooks[hook_name].append(callback)
# 记录调试日志,说明已成功注册回调
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行指定名称的动作钩子Action Hook
动作钩子用于触发一系列操作不关注返回值按注册顺序依次执行所有回调函数
参数:
hook_name: 要执行的钩子名称
*args: 传递给回调函数的位置参数
**kwargs: 传递给回调函数的关键字参数
"""
# 检查该钩子是否有注册的回调函数
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
# 遍历执行该钩子下的所有回调函数
for callback in _hooks[hook_name]:
try:
# 调用回调函数并传递参数
callback(*args, **kwargs)
except Exception as e:
# 捕获并记录回调执行过程中的异常,不中断后续回调
logger.error(
f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True # 记录完整的异常堆栈信息,便于调试
)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行指定名称的过滤钩子Filter Hook
过滤钩子用于对某个值进行一系列处理将值依次传递给所有回调函数最终返回处理后的结果
参数:
hook_name: 要执行的钩子名称
value: 初始值将被回调函数依次处理
*args: 传递给回调函数的额外位置参数
**kwargs: 传递给回调函数的额外关键字参数
返回:
经过所有回调函数处理后的最终值
"""
# 检查该钩子是否有注册的回调函数
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
# 遍历执行该钩子下的所有回调函数,依次处理值
for callback in _hooks[hook_name]:
try:
# 调用回调函数,传入当前值和其他参数,并用返回值更新当前值
value = callback(value, *args, **kwargs)
except Exception as e:
# 捕获并记录回调执行过程中的异常,不中断后续回调
logger.error(
f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True # 记录完整的异常堆栈信息,便于调试
)
# 返回最终处理后的值
return value

@ -0,0 +1,124 @@
# 插件加载与管理模块
# 该模块提供了Django应用中插件的动态加载、注册和查询功能
# 主要功能包括:从指定目录加载激活的插件、维护插件注册表、提供多种插件查询接口
# 插件需符合特定结构包含plugin.py及插件实例通过Django配置指定激活的插件和插件目录
import os
import logging
from django.conf import settings
# 初始化日志记录器,用于记录插件加载过程中的信息和错误
logger = logging.getLogger(__name__)
# 全局插件注册表,存储所有已成功加载的插件实例
_loaded_plugins = []
def load_plugins():
"""
'plugins'目录动态加载并初始化激活的插件
该函数应在Django应用注册表就绪后调用确保Django配置已加载
加载逻辑遍历配置中激活的插件列表检查插件目录结构完整性导入并初始化插件实例
"""
global _loaded_plugins
# 重置插件注册表,避免重复加载
_loaded_plugins = []
# 遍历配置中激活的插件名称列表settings.ACTIVE_PLUGINS定义需加载的插件
for plugin_name in settings.ACTIVE_PLUGINS:
# 构建插件目录的绝对路径settings.PLUGINS_DIR为插件根目录
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# 检查插件目录是否存在且包含必要的plugin.py文件插件入口
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# 动态导入插件模块从plugins.插件名.plugin导入模块
# fromlist=['plugin']确保导入子模块而非父模块
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
# 检查导入的模块是否包含'plugin'属性(插件实例)
if hasattr(plugin_module, 'plugin'):
# 获取插件实例并添加到全局注册表
plugin_instance = plugin_module.plugin
_loaded_plugins.append(plugin_instance)
# 记录成功加载日志,包含插件名称和插件定义的名称
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
else:
# 插件模块结构不完整缺少plugin实例时记录警告
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
except ImportError as e:
# 捕获导入错误(如模块不存在、依赖缺失等)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
except AttributeError as e:
# 捕获属性错误(如插件实例缺少必要属性)
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
except Exception as e:
# 捕获其他未预期的错误
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
def get_loaded_plugins():
"""
获取所有已加载的插件实例列表
返回:
list: 包含所有成功加载的插件实例的列表
"""
return _loaded_plugins
def get_plugin_by_name(plugin_name):
"""
根据插件名称查询插件实例实际查询的是plugin_slug属性可能与函数名存在命名兼容
参数:
plugin_name: 要查询的插件slug名称
返回:
匹配的插件实例若未找到则返回None
"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_name:
return plugin
return None
def get_plugin_by_slug(plugin_slug):
"""
根据插件slug查询插件实例与plugin_slug属性精确匹配
参数:
plugin_slug: 要查询的插件slug标识
返回:
匹配的插件实例若未找到则返回None
"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_slug:
return plugin
return None
def get_plugins_info():
"""
获取所有已加载插件的信息字典列表
信息由插件的get_plugin_info()方法提供通常包含名称描述版本等元数据
返回:
list: 每个元素为一个插件的信息字典
"""
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
def get_plugins_by_position(position):
"""
获取支持指定位置的所有插件实例基于插件的SUPPORTED_POSITIONS属性筛选
用于在页面特定位置渲染插件内容如侧边栏页头等
参数:
position: 位置标识'sidebar''header'对应POSITION_HOOKS中的键
返回:
list: 所有支持该位置的插件实例
"""
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]

@ -0,0 +1,384 @@
"""
Django settings for djangoblog project.
Generated by 'django-admin startproject' using Django 1.10.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
"""将环境变量值转换为布尔值的工具函数"""
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
#姜雨菲: 构建项目路径BASE_DIR为项目根目录
BASE_DIR = Path(__file__).resolve().parent.parent
#姜雨菲: 快速开发设置 - 不适用于生产环境
# 安全警告生产环境中请保持SECRET_KEY的机密性
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# 安全警告生产环境中请关闭DEBUG模式
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# 测试环境标识当执行测试命令时为True
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# 允许访问的主机,生产环境需配置具体域名
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# Django 4.0新增配置指定可信任的CSRF来源
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# 应用定义
INSTALLED_APPS = [
# 自定义的Admin配置简化版
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth', # 用户认证应用
'django.contrib.contenttypes', # 内容类型框架
'django.contrib.sessions', # 会话框架
'django.contrib.messages', # 消息框架
'django.contrib.staticfiles', # 静态文件管理
'django.contrib.sites', # 站点框架(用于多站点管理)
'django.contrib.sitemaps', # 站点地图框架
'mdeditor', # Markdown编辑器应用
'haystack', # 搜索框架
'blog', # 博客应用
'accounts', # 用户账户应用
'comments', # 评论应用
'oauth', # 第三方登录应用
'servermanager', # 服务器管理应用
'owntracks', # 位置追踪应用
'compressor', # 静态文件压缩应用
'djangoblog' # 项目主应用
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全中间件处理HTTPS等安全相关
'django.contrib.sessions.middleware.SessionMiddleware', # 会话中间件
'django.middleware.locale.LocaleMiddleware', # 国际化中间件(语言切换)
'django.middleware.gzip.GZipMiddleware', # GZip压缩中间件
'django.middleware.common.CommonMiddleware', # 通用中间件(处理请求/响应)
'django.middleware.csrf.CsrfViewMiddleware', # CSRF保护中间件
'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件
'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持保护中间件
'django.middleware.http.ConditionalGetMiddleware', # 条件获取中间件处理304响应
'blog.middleware.OnlineMiddleware' # 自定义在线用户统计中间件
]
ROOT_URLCONF = 'djangoblog.urls' # 项目URL配置入口
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates', # 模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 自定义模板目录
'APP_DIRS': True, # 是否自动搜索应用内的templates目录
'OPTIONS': {
'context_processors': [ # 模板上下文处理器
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor' # 自定义SEO上下文处理器
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application' # WSGI应用入口
# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 使用MySQL数据库引擎
'NAME': 'djangoblog', # 数据库名
'USER': 'root', # 数据库用户名
'PASSWORD': '050807', # 数据库密码
'HOST': '127.0.0.1', # 数据库主机
'PORT': 3306, # 数据库端口
}
}
# 密码验证配置
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# 支持的语言列表(国际化配置)
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
# 语言文件路径
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
LANGUAGE_CODE = 'zh-hans' # 默认语言
TIME_ZONE = 'Asia/Shanghai' # 时区
USE_I18N = True # 启用国际化
USE_L10N = True # 启用本地化格式
USE_TZ = False # 不使用时区感知模型
# 搜索框架Haystack配置
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # 使用Whoosh搜索引擎中文适配版
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), # 索引存储路径
},
}
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' # 实时信号处理器(自动更新索引)
# 认证后端配置(支持用户名或邮箱登录)
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
# 静态文件配置
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 静态文件收集目录(生产环境用)
STATIC_URL = '/static/' # 静态文件URL前缀
STATICFILES = os.path.join(BASE_DIR, 'static') # 静态文件源目录
# 插件静态文件目录
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'plugins'), # 插件静态文件目录
]
AUTH_USER_MODEL = 'accounts.BlogUser' # 自定义用户模型
LOGIN_URL = '/login/' # 登录URL
# 时间格式定义
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# Bootstrap颜色样式
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# 分页配置
PAGINATE_BY = 10
# HTTP缓存超时时间
CACHE_CONTROL_MAX_AGE = 2592000
# 缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 本地内存缓存
'TIMEOUT': 10800, # 缓存超时时间(秒)
'LOCATION': 'unique-snowflake', # 缓存位置标识
}
}
# 若存在环境变量则使用Redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
SITE_ID = 1 # 站点ID多站点时使用
# 百度链接提交通知URL
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# 邮件配置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP邮件后端
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否使用TLS
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否使用SSL
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # 邮件服务器
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # 邮件端口
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 邮件用户名
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮件密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人
SERVER_EMAIL = EMAIL_HOST_USER # 服务器邮件发件人
# 管理员邮件通知配置
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# 微信管理密码两次MD5加密
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
# 日志配置
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
},
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
# 静态文件查找器用于Compressor
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True # 启用静态文件压缩
# 根据环境变量决定是否启用离线压缩
COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
COMPRESS_OUTPUT_DIR = 'compressed' # 压缩文件输出目录
COMPRESS_CSS_HASHING_METHOD = 'mtime' # CSS哈希生成方式基于修改时间
COMPRESS_JS_HASHING_METHOD = 'mtime' # JS哈希生成方式基于修改时间
# CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
'compressor.filters.css_default.CssAbsoluteFilter', # 处理CSS中的绝对URL
'compressor.filters.cssmin.CSSCompressorFilter', # CSS压缩器
]
# JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.SlimItFilter', # JS压缩器
]
COMPRESS_CACHE_BACKEND = 'default' # 压缩缓存后端
COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey' # 缓存键生成函数
# 预编译器配置支持SCSS/SASS
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
('text/x-sass', 'django_libsass.SassCompiler'),
)
# 压缩性能优化配置
COMPRESS_MINT_DELAY = 30
COMPRESS_MTIME_DELAY = 10
COMPRESS_REBUILD_TIMEOUT = 2592000
COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor'
COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor'
# 静态文件存储带Manifest用于缓存破坏
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
# 媒体文件(用户上传文件)配置
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
# XFrameOptions配置允许同域iframe
X_FRAME_OPTIONS = 'SAMEORIGIN'
# 安全头部配置
SECURE_BROWSER_XSS_FILTER = True # 启用XSS过滤
SECURE_CONTENT_TYPE_NOSNIFF = True # 禁止内容类型嗅探
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin' # Referrer策略
# 内容安全策略CSP
CSP_DEFAULT_SRC = ["'self'"]
CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "cdn.mathjax.org", "*.googleapis.com"]
CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "*.googleapis.com", "*.gstatic.com"]
CSP_IMG_SRC = ["'self'", "data:", "*.lylinux.net", "*.gravatar.com", "*.githubusercontent.com"]
CSP_FONT_SRC = ["'self'", "*.googleapis.com", "*.gstatic.com"]
CSP_CONNECT_SRC = ["'self'"]
CSP_FRAME_SRC = ["'none'"]
CSP_OBJECT_SRC = ["'none'"]
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 默认自增字段类型
# 若存在环境变量则使用Elasticsearch作为搜索后端
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# 插件系统配置
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright', # 文章版权插件
'reading_time', # 阅读时间插件
'external_links', # 外部链接插件
'view_count', # 阅读计数插件
'seo_optimizer', # SEO优化插件
'image_lazy_loading', # 图片懒加载插件
'article_recommendation', # 文章推荐插件
]

@ -0,0 +1,82 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
"""
静态页面的站点地图类
用于生成网站中固定URL的页面如首页的站点地图条目
"""
# 页面优先级0.0-1.01.0表示最高优先级)
priority = 0.5
# 页面内容更新频率可选值always, hourly, daily, weekly, monthly, yearly, never
changefreq = 'daily'
def items(self):
"""返回需要包含在站点地图中的静态URL名称列表"""
# 这里仅包含博客首页的URL名称对应urls.py中定义的name='blog:index'
return ['blog:index', ]
def location(self, item):
"""根据items返回的URL名称生成完整URL"""
return reverse(item)
class ArticleSiteMap(Sitemap):
"""文章页面的站点地图类"""
changefreq = "monthly" # 文章内容更新频率为每月
priority = "0.6" # 文章页面优先级
def items(self):
"""返回需要包含的文章对象列表"""
# 仅包含状态为已发布status='p')的文章
return Article.objects.filter(status='p')
def lastmod(self, obj):
"""返回文章的最后修改时间(用于搜索引擎判断内容是否更新)"""
return obj.last_modify_time
class CategorySiteMap(Sitemap):
"""分类页面的站点地图类"""
changefreq = "Weekly" # 分类页面更新频率为每周
priority = "0.6" # 分类页面优先级
def items(self):
"""返回所有分类对象"""
return Category.objects.all()
def lastmod(self, obj):
"""返回分类的最后修改时间"""
return obj.last_modify_time
class TagSiteMap(Sitemap):
"""标签页面的站点地图类"""
changefreq = "Weekly" # 标签页面更新频率为每周
priority = "0.3" # 标签页面优先级(低于文章和分类)
def items(self):
"""返回所有标签对象"""
return Tag.objects.all()
def lastmod(self, obj):
"""返回标签的最后修改时间"""
return obj.last_modify_time
class UserSiteMap(Sitemap):
"""用户页面的站点地图类"""
changefreq = "Weekly" # 用户页面更新频率为每周
priority = "0.3" # 用户页面优先级
def items(self):
"""返回所有发布过文章的作者(去重处理)"""
# 通过文章作者去重,获取所有有发布文章的用户
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""返回用户的注册时间(作为最后修改时间的替代)"""
return obj.date_joined

@ -0,0 +1,45 @@
import logging
import requests
from django.conf import settings
# 创建当前模块的日志记录器,用于记录通知相关的日志信息
logger = logging.getLogger(__name__)
class SpiderNotify():
"""
搜索引擎爬虫通知类
用于向搜索引擎目前支持百度提交网站URL告知内容更新便于爬虫抓取
"""
@staticmethod
def baidu_notify(urls):
"""
向百度搜索引擎提交URL的静态方法
通过百度链接提交通知接口告知百度新增/更新的页面URL
Args:
urls (list): 需要提交的URL列表每个元素为完整的页面URL字符串
"""
try:
# 将URL列表用换行符拼接符合百度接口的数据格式要求
data = '\n'.join(urls)
# 向百度通知接口发送POST请求提交URL数据
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# 记录接口返回的响应信息日志级别INFO
logger.info(result.text)
except Exception as e:
# 捕获所有异常并记录错误信息日志级别ERROR
logger.error(e)
@staticmethod
def notify(url):
"""
通用通知入口静态方法
统一调用百度通知方法便于后续扩展支持其他搜索引擎
Args:
url (list): 需要提交的URL列表与baidu_notify方法的urls参数格式一致
"""
SpiderNotify.baidu_notify(url)

@ -0,0 +1,51 @@
from django.test import TestCase
#姜雨菲: 导入项目工具模块中的所有工具函数/类
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
"""
博客项目核心工具类的单元测试类
用于验证工具函数的功能正确性
"""
def setUp(self):
"""
测试前置方法
在每个测试方法执行前调用可用于初始化测试数据
此处暂无需初始化操作保持空实现
"""
pass
def test_utils(self):
"""
测试工具函数的功能
包括SHA256加密Markdown解析和字典转URL参数功能
"""
# 测试SHA256加密函数
md5 = get_sha256('test') # 对字符串'test'进行SHA256加密
self.assertIsNotNone(md5) # 断言加密结果不为空
# 测试Markdown解析功能
# 定义一段包含标题、代码块、链接的Markdown文本
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)

@ -0,0 +1,89 @@
"""djangoblog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from django.http import JsonResponse
import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site # 自定义的admin站点
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # ElasticSearch搜索表单
from djangoblog.feeds import DjangoBlogFeed # RSS订阅源
from djangoblog.sitemap import ( # 站点地图相关类
ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
)
#姜雨菲: 站点地图配置:将不同类型的页面分别映射到对应的站点地图类
sitemaps = {
'blog': ArticleSiteMap, # 文章页面
'Category': CategorySiteMap, # 分类页面
'Tag': TagSiteMap, # 标签页面
'User': UserSiteMap, # 用户页面
'static': StaticViewSitemap # 静态页面
}
#姜雨菲: 自定义错误页面处理视图
handler404 = 'blog.views.page_not_found_view' # 404页面未找到
handler500 = 'blog.views.server_error_view' # 500服务器错误
handle403 = 'blog.views.permission_denied_view' # 403权限拒绝
def health_check(request):
"""
健康检查接口
用于监控服务是否正常运行简单返回服务健康状态和时间戳
"""
return JsonResponse({
'status': 'healthy', # 健康状态标识
'timestamp': time.time() # 当前时间戳
})
# 基础URL配置不包含国际化前缀
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), # 国际化配置入口
path('health/', health_check, name='health_check'), # 健康检查接口
]
# 包含国际化前缀的URL配置会自动添加语言代码前缀如/en/、/zh-hans/
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls), # 自定义admin后台URL
re_path(r'', include('blog.urls', namespace='blog')), # 博客应用URL命名空间blog
re_path(r'mdeditor/', include('mdeditor.urls')), # Markdown编辑器URL
re_path(r'', include('comments.urls', namespace='comment')), # 评论应用URL命名空间comment
re_path(r'', include('accounts.urls', namespace='account')), # 账户应用URL命名空间account
re_path(r'', include('oauth.urls', namespace='oauth')), # 第三方登录应用URL命名空间oauth
# 站点地图XML
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()), # RSS订阅源URL
re_path(r'^rss/$', DjangoBlogFeed()), # 另一个RSS订阅源URL与feed功能相同
# 搜索功能URL使用自定义的EsSearchView和搜索表单
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')), # 服务器管理应用URL
re_path(r'', include('owntracks.urls', namespace='owntracks')), # 位置追踪应用URL
prefix_default_language=False # 不为主语言添加前缀
) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # 静态文件URL配置
# 开发环境下添加媒体文件用户上传文件的URL配置
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -0,0 +1,372 @@
#!/usr/bin/env python
# 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
#姜雨菲: 创建当前模块的日志记录器
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""
获取最新文章和评论的ID
用于获取当前系统中最新发布的文章ID和最新的评论ID
"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""
对字符串进行SHA256加密
:param str: 需要加密的字符串
:return: 加密后的十六进制字符串
"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
"""
缓存装饰器
用于缓存函数返回结果减少重复计算默认缓存3分钟
:param expiration: 缓存过期时间
:return: 装饰器函数
"""
def wrapper(func):
def news(*args, **kwargs):
try:
# 尝试从第一个参数(通常是视图实例)获取缓存键
view = args[0]
key = view.get_cache_key()
except:
# 获取失败时自动生成缓存键
key = None
if not key:
# 根据函数和参数生成唯一字符串,用于创建缓存键
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
# 尝试从缓存获取数据
value = cache.get(key)
if value is not 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)
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):
'''
刷新视图缓存
手动删除指定URL路径的视图缓存
:param path: URL路径
:param servername: 主机名
:param serverport: 端口号
:param key_prefix: 缓存键前缀
:return: 是否刷新成功布尔值
'''
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()
def get_current_site():
"""
获取当前站点信息带缓存
从Django的Site模型获取当前站点配置结果缓存3分钟
:return: Site模型实例
"""
site = Site.objects.get_current()
return site
class CommonMarkdown:
"""
Markdown解析工具类
提供Markdown文本转HTML的功能支持代码高亮目录生成等
"""
@staticmethod
def _convert_markdown(value):
"""
内部Markdown转换方法
配置Markdown解析器并转换文本
:param value: Markdown格式文本
:return: (转换后的HTML内容, 目录HTML)
"""
md = markdown.Markdown(
extensions=[
'extra', # 额外功能(表格、脚注等)
'codehilite', # 代码高亮
'toc', # 目录生成
'tables', # 表格支持
]
)
body = md.convert(value)
toc = md.toc
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""
Markdown转HTML带目录
:param value: Markdown格式文本
:return: (HTML内容, 目录HTML)
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""
Markdown转HTML仅内容不含目录
:param value: Markdown格式文本
:return: 转换后的HTML内容
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""
发送邮件通过信号机制
触发邮件发送信号解耦邮件发送逻辑
:param emailto: 收件人邮箱
:param title: 邮件标题
:param content: 邮件内容
"""
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""生成6位随机数字验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""
将字典转换为URL查询字符串
对键值对进行URL编码避免特殊字符问题
:param dict: 待转换的字典
:return: URL查询字符串格式key1=value1&key2=value2
"""
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
"""
获取博客系统配置带缓存
从数据库获取博客全局配置无配置时创建默认配置结果缓存
:return: 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()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
'''
保存用户头像到本地静态文件目录
从URL下载头像并保存支持常见图片格式失败时返回默认头像
:param url: 头像图片URL
:return: 本地头像的静态文件URL
'''
logger.info(url)
try:
# 定义头像保存目录
basedir = os.path.join(settings.STATICFILES, 'avatar')
# 下载头像图片
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
# 目录不存在时创建
if not os.path.exists(basedir):
os.makedirs(basedir)
# 验证图片格式
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
# 生成唯一文件名
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():
"""
删除侧边栏缓存
根据LinkShowType的所有值生成缓存键并删除
"""
from blog.models import LinkShowType
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):
"""
删除模板片段缓存
:param prefix: 缓存前缀
:param keys: 缓存键列表
"""
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
优先使用settings中的STATIC_URL无配置时使用站点域名拼接/static/
:return: 静态资源基础URL
"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
# HTML清理配置 - 防止XSS攻击
# 允许的HTML标签白名单
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p', 'span', 'div']
# 允许的CSS类白名单主要用于代码高亮
ALLOWED_CLASSES = [
'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',
'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
]
def class_filter(tag, name, value):
"""
自定义class属性过滤器
只保留ALLOWED_CLASSES中的CSS类过滤危险或未授权的类名
:param tag: HTML标签名
:param name: 属性名
:param value: 属性值
:return: 过滤后的属性值无合法类时返回False
"""
if name == 'class':
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
return ' '.join(allowed_classes) if allowed_classes else False
return value
# 允许的HTML属性白名单
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'], # 链接标签允许的属性
'abbr': ['title'], # 缩写标签允许的属性
'acronym': ['title'], # 首字母缩写标签允许的属性
'span': class_filter, # span标签的class属性使用自定义过滤器
'div': class_filter, # div标签的class属性使用自定义过滤器
'pre': class_filter, # pre标签的class属性使用自定义过滤器
'code': class_filter # code标签的class属性使用自定义过滤器
}
# 允许的URL协议白名单防止javascript:等危险协议)
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html):
"""
安全的HTML清理函数
使用bleach库过滤危险HTML内容防止XSS攻击
:param html: 需要清理的HTML字符串
:return: 安全的HTML字符串
"""
return bleach.clean(
html,
tags=ALLOWED_TAGS, # 只允许白名单中的标签
attributes=ALLOWED_ATTRIBUTES, # 只允许白名单中的属性
protocols=ALLOWED_PROTOCOLS, # 限制URL协议
strip=True, # 移除不允许的标签(而非转义)
strip_comments=True # 移除HTML注释
)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,21 @@
"""
WSGI config for djangoblog project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
# 导入Django的WSGI应用获取函数
from django.core.wsgi import get_wsgi_application
#姜雨菲: 设置Django默认的配置模块环境变量
#姜雨菲: 告诉Django使用哪个settings.py文件这里指定为项目的djangoblog.settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
#姜雨菲: 创建WSGI应用实例
#姜雨菲: 这个application变量会被WSGI服务器如Gunicorn、uWSGI调用处理HTTP请求
application = get_wsgi_application()
Loading…
Cancel
Save