├── job ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── forms.py ├── apps.py ├── admin.py ├── urls.py ├── static │ └── job │ │ ├── css │ │ └── cover.css │ │ └── js │ │ └── bootstrap.min.js ├── tasks.py ├── templates │ └── job │ │ ├── scan.html │ │ ├── upload.html │ │ ├── search.html │ │ └── count.html ├── models.py └── views.py ├── user ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── static │ └── user │ │ ├── image │ │ └── bg.jpg │ │ ├── css │ │ ├── login.css │ │ └── cover.css │ │ └── js │ │ └── bootstrap.min.js ├── apps.py ├── admin.py ├── urls.py ├── models.py ├── templates │ └── user │ │ ├── forget_confirm.html │ │ ├── confirm.html │ │ ├── forget_index.html │ │ ├── register.html │ │ ├── login.html │ │ └── forget_change.html ├── forms.py └── views.py ├── utils ├── __init__.py ├── utils.py └── scanFile.py ├── TrainedData ├── cv.pkl ├── gnb.pkl └── tf.pkl ├── ScanWebShell ├── __init__.py ├── views.py ├── asgi.py ├── wsgi.py ├── urls.py ├── celery.py ├── static │ └── ScanWebShell │ │ └── css │ │ └── cover.css ├── templates │ └── ScanWebShell │ │ └── index.html └── settings.example.py ├── manage.py ├── requirements.txt ├── README.md ├── learn.yaml └── 开发文档.md /job/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /job/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /job/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /user/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /TrainedData/cv.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fe1w0/ScanWebShell/HEAD/TrainedData/cv.pkl -------------------------------------------------------------------------------- /TrainedData/gnb.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fe1w0/ScanWebShell/HEAD/TrainedData/gnb.pkl -------------------------------------------------------------------------------- /TrainedData/tf.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fe1w0/ScanWebShell/HEAD/TrainedData/tf.pkl -------------------------------------------------------------------------------- /job/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class UploadFileForm(forms.Form): 4 | file = forms.FileField() -------------------------------------------------------------------------------- /user/static/user/image/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fe1w0/ScanWebShell/HEAD/user/static/user/image/bg.jpg -------------------------------------------------------------------------------- /job/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class JobConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'job' 7 | -------------------------------------------------------------------------------- /user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'user' 7 | -------------------------------------------------------------------------------- /user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | 5 | from . import models 6 | 7 | admin.site.register(models.User) 8 | admin.site.register(models.ConfirmString) -------------------------------------------------------------------------------- /job/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | # Register your models here. 5 | admin.site.register(models.ModelWithFileField) 6 | admin.site.register(models.ScanTaskField) -------------------------------------------------------------------------------- /ScanWebShell/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery import app as celery_app 4 | 5 | __all__ = ('celery_app',) -------------------------------------------------------------------------------- /job/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'job' 6 | urlpatterns = [ 7 | path('upload/', views.upload_file), 8 | path('scan/', views.scan_file), 9 | path('search/',views.searchResult), 10 | path('count/',views.countResult) 11 | ] -------------------------------------------------------------------------------- /ScanWebShell/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def index(request): 5 | """ 6 | 根据session 放回不同的首页[已登录,未登录] 7 | :param request: 8 | :return: 9 | """ 10 | if request.session.get('is_login'): 11 | is_login = True # 在模板层启用不同的 引导航 12 | return render(request, 'ScanWebShell/index.html', locals()) 13 | -------------------------------------------------------------------------------- /ScanWebShell/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for ScanWebShell project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ScanWebShell.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ScanWebShell/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for ScanWebShell project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ScanWebShell.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ScanWebShell/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | from django.urls import path,include 4 | from django.views.static import serve 5 | from ScanWebShell import views 6 | from django.conf import settings 7 | 8 | 9 | urlpatterns = [ 10 | path('', views.index), # 首页 11 | path('index/', views.index), # 首页 12 | path('admin/', admin.site.urls), 13 | path('user/', include('user.urls')), 14 | path('job/',include('job.urls')), 15 | path('captcha/', include('captcha.urls')), 16 | url(r"^static/(?P.*)$", serve, {"document_root": settings.STATIC_ROOT}, name='static'), 17 | ] 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ScanWebShell.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /user/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.urls import path 3 | 4 | from . import views 5 | from captcha.views import captcha_refresh 6 | 7 | app_name = 'user' 8 | urlpatterns = [ 9 | path('', views.index, name = 'index'), 10 | path('index/', views.index, name = 'index'), 11 | path('login/', views.login, name = 'login'), 12 | path('register/', views.register, name = 'register'), 13 | path('logout/', views.logout, name = 'logout'), 14 | path('confirm/', views.user_confirm), 15 | path('forget/',views.forget_index), 16 | path('forget/index/',views.forget_index), 17 | path('forget/confirm/',views.forget_confirm), 18 | path('forget/change/',views.forget_change), 19 | url(r'^refresh/',captcha_refresh) 20 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==5.0.6 2 | asgiref==3.3.4 3 | astroid==2.4.2 4 | billiard==3.6.4.0 5 | celery==5.0.5 6 | certifi==2020.12.5 7 | cffi==1.14.5 8 | chardet==4.0.0 9 | click==7.1.2 10 | click-didyoumean==0.0.3 11 | click-plugins==1.1.1 12 | click-repl==0.1.6 13 | colorama==0.4.3 14 | Django==3.2 15 | django-celery-results==2.0.1 16 | django-ranged-response==0.2.0 17 | django-simple-captcha==0.5.14 18 | humanize==3.4.1 19 | idna==2.10 20 | joblib==1.0.1 21 | mccabe==0.6.1 22 | msgpack==1.0.2 23 | Pillow==8.1.2 24 | prometheus-client==0.8.0 25 | prompt-toolkit==3.0.18 26 | pycparser==2.20 27 | pylint==2.6.0 28 | redis==3.5.3 29 | requests==2.25.1 30 | toml==0.10.1 31 | tornado==6.1 32 | scikit-learn==0.24.1 33 | urllib3==1.26.4 34 | vine==5.0.0 35 | wcwidth==0.2.5 36 | wincertstore==0.2 37 | wrapt==1.12.1 -------------------------------------------------------------------------------- /ScanWebShell/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | from celery import Celery 5 | 6 | # set the default Django settings module for the 'celery' program. 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ScanWebShell.settings') 8 | 9 | app = Celery('ScanWebShell') 10 | 11 | # Using a string here means the worker doesn't have to serialize 12 | # the configuration object to child processes. 13 | # - namespace='CELERY' means all celery-related configuration keys 14 | # should have a `CELERY_` prefix. 15 | app.config_from_object('django.conf:settings', namespace='CELERY') 16 | 17 | # Load task modules from all registered Django app configs. 18 | app.autodiscover_tasks() 19 | 20 | 21 | @app.task(bind=True) 22 | def debug_task(self): 23 | print(f'Request: {self.request!r}') -------------------------------------------------------------------------------- /user/static/user/css/login.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | } 4 | .form-login { 5 | width: 100%; 6 | max-width: 330px; 7 | padding: 15px; 8 | margin: 0 auto; 9 | } 10 | .form-login{ 11 | margin-top:80px; 12 | font-weight: 400; 13 | } 14 | .form-login .form-control { 15 | position: relative; 16 | box-sizing: border-box; 17 | height: auto; 18 | padding: 10px; 19 | font-size: 16px; 20 | 21 | } 22 | .form-login .form-control:focus { 23 | z-index: 2; 24 | } 25 | .form-login input[type="text"] { 26 | margin-bottom: -1px; 27 | border-bottom-right-radius: 0; 28 | border-bottom-left-radius: 0; 29 | } 30 | .form-login input[type="password"] { 31 | margin-bottom: 10px; 32 | border-top-left-radius: 0; 33 | border-top-right-radius: 0; 34 | } 35 | form a{ 36 | display: inline-block; 37 | margin-top:25px; 38 | font-size: 12px; 39 | line-height: 10px; 40 | } -------------------------------------------------------------------------------- /job/static/job/css/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | 6 | /* Custom default button */ 7 | .btn-secondary, 8 | .btn-secondary:hover, 9 | .btn-secondary:focus { 10 | color: #333; 11 | text-shadow: none; /* Prevent inheritance from `body` */ 12 | } 13 | 14 | 15 | /* 16 | * Base structure 17 | */ 18 | 19 | body { 20 | text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); 21 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); 22 | } 23 | 24 | .cover-container { 25 | width: 70em; 26 | } 27 | 28 | 29 | /* 30 | * Header 31 | */ 32 | 33 | .nav-masthead .nav-link { 34 | padding: .25rem 0; 35 | font-weight: 700; 36 | color: rgba(255, 255, 255, .5); 37 | background-color: transparent; 38 | border-bottom: .25rem solid transparent; 39 | } 40 | 41 | .nav-masthead .nav-link:hover, 42 | .nav-masthead .nav-link:focus { 43 | border-bottom-color: rgba(255, 255, 255, .25); 44 | } 45 | 46 | .nav-masthead .nav-link + .nav-link { 47 | margin-left: 1rem; 48 | } 49 | 50 | .nav-masthead .active { 51 | color: #fff; 52 | border-bottom-color: #fff; 53 | } 54 | -------------------------------------------------------------------------------- /user/static/user/css/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | 6 | /* Custom default button */ 7 | .btn-secondary, 8 | .btn-secondary:hover, 9 | .btn-secondary:focus { 10 | color: #333; 11 | text-shadow: none; /* Prevent inheritance from `body` */ 12 | } 13 | 14 | 15 | /* 16 | * Base structure 17 | */ 18 | 19 | body { 20 | text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); 21 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); 22 | } 23 | 24 | .cover-container { 25 | width: 70em; 26 | } 27 | 28 | 29 | /* 30 | * Header 31 | */ 32 | 33 | .nav-masthead .nav-link { 34 | padding: .25rem 0; 35 | font-weight: 700; 36 | color: rgba(255, 255, 255, .5); 37 | background-color: transparent; 38 | border-bottom: .25rem solid transparent; 39 | } 40 | 41 | .nav-masthead .nav-link:hover, 42 | .nav-masthead .nav-link:focus { 43 | border-bottom-color: rgba(255, 255, 255, .25); 44 | } 45 | 46 | .nav-masthead .nav-link + .nav-link { 47 | margin-left: 1rem; 48 | } 49 | 50 | .nav-masthead .active { 51 | color: #fff; 52 | border-bottom-color: #fff; 53 | } 54 | -------------------------------------------------------------------------------- /user/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class User(models.Model): 5 | """ 6 | 用户模型 7 | """ 8 | name = models.CharField(max_length=128,unique=True) 9 | password = models.CharField(max_length=256) 10 | email = models.EmailField(unique=True) 11 | c_time = models.DateTimeField(auto_now_add=True) 12 | has_confirmed = models.BooleanField(default=False) 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | class Meta: 18 | ordering = ["-c_time"] 19 | verbose_name = "用户" 20 | verbose_name_plural = "用户" 21 | 22 | 23 | class ConfirmString(models.Model): 24 | """ 25 | 邮箱确认模型 26 | """ 27 | code = models.CharField(max_length=256) 28 | user = models.OneToOneField('User', on_delete=models.CASCADE) 29 | c_time = models.DateTimeField(auto_now_add=True) 30 | 31 | def __str__(self): 32 | return self.user.name + ": " + self.code 33 | 34 | class Meta: 35 | 36 | ordering = ["-c_time"] 37 | verbose_name = "确认码" 38 | verbose_name_plural = "确认码" -------------------------------------------------------------------------------- /ScanWebShell/static/ScanWebShell/css/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | 6 | /* Custom default button */ 7 | .btn-secondary, 8 | .btn-secondary:hover, 9 | .btn-secondary:focus { 10 | color: #333; 11 | text-shadow: none; /* Prevent inheritance from `body` */ 12 | } 13 | 14 | 15 | /* 16 | * Base structure 17 | */ 18 | 19 | body { 20 | text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); 21 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); 22 | } 23 | 24 | .cover-container { 25 | width: 70em; 26 | } 27 | 28 | 29 | /* 30 | * Header 31 | */ 32 | 33 | .nav-masthead .nav-link { 34 | padding: .25rem 0; 35 | font-weight: 700; 36 | color: rgba(255, 255, 255, .5); 37 | background-color: transparent; 38 | border-bottom: .25rem solid transparent; 39 | } 40 | 41 | .nav-masthead .nav-link:hover, 42 | .nav-masthead .nav-link:focus { 43 | border-bottom-color: rgba(255, 255, 255, .25); 44 | } 45 | 46 | .nav-masthead .nav-link + .nav-link { 47 | margin-left: 1rem; 48 | } 49 | 50 | .nav-masthead .active { 51 | color: #fff; 52 | border-bottom-color: #fff; 53 | } 54 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import re 4 | 5 | 6 | def recursion_load_php_file_opcode(dir): 7 | """ 8 | 递归获取 php opcde 9 | :param dir: 目录文件 10 | :return: 11 | """ 12 | files_list = [] 13 | for root, dirs, files in os.walk(dir): 14 | for filename in files: 15 | if filename.endswith('.php') or filename.endswith('.php3') or filename.endswith('.php5') or filename.endswith('.phtml') or filename.endswith('.pht') : 16 | try: 17 | full_path = os.path.join(root, filename) 18 | file_content = load_php_opcode(full_path) 19 | print("[Gen success] {}".format(full_path)) 20 | print('--' * 20) 21 | if file_content != "": 22 | files_list.append(file_content) 23 | except: 24 | continue 25 | return files_list 26 | 27 | 28 | 29 | def load_php_opcode(phpfilename): 30 | """ 31 | 获取php opcode 信息 32 | :param phpfilename: 33 | :return: 34 | """ 35 | try: 36 | output = subprocess.check_output(['php', '-dvld.active=1', '-dvld.execute=0', phpfilename], stderr=subprocess.STDOUT).decode() 37 | tokens = re.findall(r'\s(\b[A-Z_]{2,}\b)\s', output) # {2,} 至少匹配两次,规避一开始三个错误的结果 38 | t = " ".join(tokens) 39 | return t 40 | except BaseException as e : 41 | return "" 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | ScanWebShell 为基于机器学习的PHP-WebShell扫描工具,该版本为web服务形式。支持多用户独立使用和利用`celery`来配合扫描任务。 3 | 4 | * `index`![image-20210421173150850](http://img.xzaslxr.xyz/image-20210421173150850.png) 5 | 6 | * `job/count`![image-20210421180506546](http://img.xzaslxr.xyz/image-20210421180506546.png) 7 | 8 | 9 | # Usage 10 | 11 | 12 | * 下载 13 | ```bash 14 | git clone https://github.com/fe1w0/ScanWebShell.git 15 | cd ScanWebShell 16 | ``` 17 | 18 | * 配置环境 19 | * `php vld` 插件安装 20 | http://pecl.php.net/package/vld 21 | 安装后`php -m`来确定是否安装 22 | * `settings.py` 23 | ```bash 24 | cp ScanWebShell/settings.example.py ScanWebShell/settings.py 25 | ``` 26 | 出于安全角度,`SECRET_KEY`参数强烈建议修改,修改方法如下: 27 | ```python 28 | #进入Django shell 29 | #python3 manage.py shell 30 | #加载utils模块 31 | from django.core.management import utils 32 | #生成密钥 33 | utils.get_random_secret_key() 34 | ``` 35 | 邮箱(用于注册和重置密码功能)还需要在`settings.py`中配置如下参数: 36 | 37 | ![image-20210421180717445](http://img.xzaslxr.xyz/image-20210421180717445.png) 38 | 39 | 40 | ```bash 41 | python3 -m pip install -r requirements.txt 42 | python3 manage.py makemigrations 43 | python3 manage.py migrate 44 | python3 manage.py collectstatic 45 | python3 manage.py createsuperuser 46 | ``` 47 | 48 | 在`celery`中设置`worker`为`redis`,需要 49 | ```bash 50 | docker pull redis:latest 51 | docker run --name=redis -d -p 6379:6379 redis 52 | ``` 53 | 54 | * `celery`启动 55 | ```bash 56 | celery -A ScanWebShell worker -l info # 可以配合tmux或后台运行工具 57 | ``` 58 | * runserver 59 | ```bash 60 | python3 manage.py runserver 0.0.0.0:8000 61 | ``` 62 | -------------------------------------------------------------------------------- /job/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | import os 5 | import zipfile 6 | import shutil 7 | from celery import shared_task 8 | from utils.scanFile import webshellScan 9 | from celery import Task 10 | from job.models import ScanTaskField 11 | from django_celery_results.models import TaskResult 12 | 13 | 14 | def unzip_function(zip_file_name, path="."): 15 | with zipfile.ZipFile(zip_file_name, "r") as zip_obj: 16 | zip_obj.extractall(path=path) 17 | 18 | 19 | class processTask(Task): 20 | """ 21 | 更新数据库 22 | """ 23 | def after_return(self, status, retval, task_id, args, kwargs, einfo): 24 | scan_task = ScanTaskField.objects.get(task_id=task_id) 25 | scan_task.task_result = TaskResult.objects.get(task_id=task_id) 26 | scan_task.task_status = TaskResult.objects.get(task_id=task_id).status 27 | scan_task.save() 28 | 29 | 30 | @shared_task(base=processTask) 31 | def scanTask(file_name): 32 | """ 33 | 创建扫描任务 34 | :param file_name: 35 | :return: 36 | """ 37 | if file_name.endswith("zip"): 38 | unzip_path = file_name[:-4] 39 | os.mkdir(unzip_path) 40 | unzip_function(file_name, unzip_path) 41 | result_array = webshellScan(unzip_path) 42 | result_json = json.dumps(result_array) 43 | # 任务完成后,删除解压后的文件夹 44 | shutil.rmtree(unzip_path) # 递归删除文件夹,即:删除非空文件夹 45 | if file_name.endswith("php"): 46 | result_array = webshellScan(file_name) 47 | result_json = json.dumps(result_array) 48 | return result_json 49 | -------------------------------------------------------------------------------- /user/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-16 10:49 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='User', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=128, unique=True)), 20 | ('password', models.CharField(max_length=256)), 21 | ('email', models.EmailField(max_length=254, unique=True)), 22 | ('c_time', models.DateTimeField(auto_now_add=True)), 23 | ('has_confirmed', models.BooleanField(default=False)), 24 | ], 25 | options={ 26 | 'verbose_name': '用户', 27 | 'verbose_name_plural': '用户', 28 | 'ordering': ['-c_time'], 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='ConfirmString', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('code', models.CharField(max_length=256)), 36 | ('c_time', models.DateTimeField(auto_now_add=True)), 37 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='user.user')), 38 | ], 39 | options={ 40 | 'verbose_name': '确认码', 41 | 'verbose_name_plural': '确认码', 42 | 'ordering': ['-c_time'], 43 | }, 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /user/templates/user/forget_confirm.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 忘记密码 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |

ScanWebShell

37 | 41 |
42 |
43 | 44 |
45 | {% if message %} 46 |
{{ message }}
47 | {% endif %} 48 | {% csrf_token %} 49 |
50 | 51 | 54 | 55 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /job/templates/job/scan.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Scan 8 | 9 | 10 | 11 | 12 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |

ScanWebShell

36 | 42 |
43 |
44 | 45 |
46 | {% if message_warning %} 47 | 48 | {% endif %} 49 | {% if message_success %} 50 | 51 | {% endif %} 52 |
53 | 54 | 57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /job/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-16 12:10 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('user', '0001_initial'), 13 | # ('django_celery_results', '0009_auto_20210416_2010'), 该参数可能会有问题 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ScanTaskField', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('c_time', models.DateTimeField(auto_now_add=True)), 22 | ('scan_file_name', models.CharField(max_length=256, null=True)), 23 | ('task_id', models.CharField(max_length=256, null=True)), 24 | ('task_status', models.CharField(default='PENDING', max_length=256)), 25 | ('task_creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='user.user')), 26 | ('task_result', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_results.taskresult')), 27 | ], 28 | options={ 29 | 'verbose_name': '任务表单', 30 | 'verbose_name_plural': '任务表单', 31 | 'ordering': ['-c_time'], 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='ModelWithFileField', 36 | fields=[ 37 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('tmp_file', models.FileField(upload_to='./FileUpload/')), 39 | ('c_time', models.DateTimeField(auto_now_add=True)), 40 | ('file_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='user.user')), 41 | ], 42 | options={ 43 | 'verbose_name': '文件', 44 | 'verbose_name_plural': '文件', 45 | 'ordering': ['-c_time'], 46 | }, 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /user/templates/user/confirm.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 注册确认 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |

ScanWebShell

37 | 41 |
42 |
43 | 44 |
45 | {% if message_warning %} 46 | 47 | {% endif %} 48 | {% if message_success %} 49 | 50 | {% endif %} 51 |
52 | 53 | 56 | 57 | 60 |
61 | 62 | -------------------------------------------------------------------------------- /job/models.py: -------------------------------------------------------------------------------- 1 | from django_celery_results.models import TaskResult 2 | from user.models import User 3 | import os 4 | from django.db import models 5 | from django.db.models.signals import post_delete 6 | from django.dispatch import receiver 7 | from django.conf import settings 8 | 9 | 10 | class ModelWithFileField(models.Model): 11 | tmp_file = models.FileField(upload_to = './FileUpload/') 12 | file_user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) 13 | ''' 14 | 值得注意的一点是,FileUpload中已经存在相同文件名的文件时,会对上传文件的文件名重命名 15 | 如 1.png 转为 1_fIZVhN3.png 16 | 且存储的文件为 1_fIZVhN3.png 17 | ''' 18 | c_time = models.DateTimeField(auto_now_add=True) 19 | 20 | def __str__(self): 21 | return self.tmp_file.name 22 | 23 | class Meta: 24 | ordering = ["-c_time"] 25 | verbose_name = "文件" 26 | verbose_name_plural = "文件" 27 | 28 | 29 | # 添加装饰器 30 | @receiver(post_delete, sender=ModelWithFileField) 31 | def delete_upload_files(sender, instance, **kwargs): 32 | files = getattr(instance, 'tmp_file') 33 | if not files: 34 | return 35 | fname = os.path.join(settings.MEDIA_ROOT, str(files)) 36 | if os.path.isfile(fname): 37 | os.remove(fname) 38 | 39 | 40 | class ScanTaskField(models.Model): 41 | # 创建时间 42 | c_time = models.DateTimeField(auto_now_add=True) 43 | # ScanFileName 44 | scan_file_name = models.CharField(max_length=256, null=True) 45 | # task_id task_status 虽然增加了整个系统的复杂度,但目前没有找到好一点的方法。 46 | # AsyncResult 中 得到的 task_id 47 | task_id = models.CharField(max_length=256, null=True) 48 | # task_status,可以通过AsyncResult(task_id).status 查询 49 | task_status = models.CharField(max_length=256, default="PENDING") 50 | # celery 中的 taskResult,等任务执行后添加进去 51 | task_result = models.OneToOneField( 52 | TaskResult, 53 | on_delete=models.CASCADE, 54 | null=True 55 | ) 56 | # ScanTask 与 User 进行多对一绑定 57 | task_creator = models.ForeignKey( 58 | User, 59 | on_delete=models.CASCADE, 60 | null=True 61 | ) 62 | 63 | def __str__(self): 64 | return self.scan_file_name 65 | 66 | class Meta: 67 | ordering = ["-c_time"] 68 | verbose_name = "任务表单" 69 | verbose_name_plural = "任务表单" -------------------------------------------------------------------------------- /user/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from captcha.fields import CaptchaField 3 | 4 | 5 | class UserForm(forms.Form): 6 | username = forms.CharField( 7 | # label="用户名", 8 | max_length=128, 9 | widget=forms.TextInput( 10 | attrs={ 11 | 'class': 'form-control', 12 | 'placeholder': 'Username', 13 | 'autofocus': ''}) 14 | ) 15 | password = forms.CharField( 16 | # label="密码", 17 | max_length=256, 18 | widget=forms.PasswordInput( 19 | attrs={ 20 | 'class': 'form-control', 21 | 'placeholder': "Password" 22 | } 23 | ) 24 | ) 25 | captcha = CaptchaField() 26 | 27 | 28 | class RegisterForm(forms.Form): 29 | username = forms.CharField( 30 | label="用户名", 31 | max_length=128, 32 | widget=forms.TextInput( 33 | attrs={ 34 | 'class': 'form-control' 35 | } 36 | ) 37 | ) 38 | password1 = forms.CharField( 39 | label="密码", 40 | max_length=256, 41 | widget=forms.PasswordInput( 42 | attrs={ 43 | 'class': 'form-control' 44 | } 45 | ) 46 | ) 47 | password2 = forms.CharField( 48 | label="确认密码", 49 | max_length=256, 50 | widget=forms.PasswordInput( 51 | attrs={ 52 | 'class': 'form-control' 53 | } 54 | ) 55 | ) 56 | email = forms.EmailField( 57 | label="邮箱地址", 58 | widget=forms.EmailInput( 59 | attrs={ 60 | 'class': 'form-control' 61 | } 62 | ) 63 | ) 64 | captcha = CaptchaField(label="验证码") 65 | 66 | 67 | class ForgetIndexForm(forms.Form): 68 | email = forms.EmailField(label="邮箱地址", widget=forms.EmailInput(attrs={'class': 'form-control'})) 69 | captcha = CaptchaField(label="验证码") 70 | 71 | 72 | class ForgetChangeForm(forms.Form): 73 | password1 = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) 74 | password2 = forms.CharField(label="确认密码", max_length=256, 75 | widget=forms.PasswordInput(attrs={'class': 'form-control'})) 76 | code = forms.CharField(max_length=256) 77 | captcha = CaptchaField(label="验证码") 78 | -------------------------------------------------------------------------------- /job/templates/job/upload.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Upload 8 | 9 | 10 | 11 | 12 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |

ScanWebShell

36 | 41 |
42 |
43 | 44 |
45 |
46 | {% csrf_token %} 47 | 48 | 49 | 50 | 51 | {% if message_warning %} 52 | 53 | {% endif %} 54 | {% if message_success %} 55 | 56 | {% endif %} 57 | 58 | 59 |
60 |
61 | 62 | 65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /ScanWebShell/templates/ScanWebShell/index.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | ScanWebShell 8 | 9 | 10 | 11 | 12 | 13 | 14 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |

ScanWebShell

40 | 49 |
50 |
51 | 52 |
53 |

An AI powered web-shell scanner.

54 |

Cover is a one-page template for building simple and beautiful home pages. Download, edit the 55 | text, and add your own fullscreen background photo to make it your own.

56 |

57 | Get Start 58 |

59 |
60 | 61 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /learn.yaml: -------------------------------------------------------------------------------- 1 | name: learn 2 | channels: 3 | - https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge 4 | - https://mirrors.ustc.edu.cn/anaconda/cloud/menpo/ 5 | - https://mirrors.ustc.edu.cn/anaconda/cloud/bioconda/ 6 | - https://mirrors.ustc.edu.cn/anaconda/cloud/msys2/ 7 | - https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge/ 8 | - https://mirrors.ustc.edu.cn/anaconda/pkgs/free/ 9 | - https://mirrors.ustc.edu.cn/anaconda/pkgs/main/ 10 | - defaults 11 | dependencies: 12 | - blas=1.0=mkl 13 | - ca-certificates=2020.12.5=h5b45459_0 14 | - certifi=2020.12.5=py38haa244fe_1 15 | - icc_rt=2019.0.0=h0cc432a_1 16 | - intel-openmp=2020.2=254 17 | - joblib=1.0.1=pyhd3eb1b0_0 18 | - mkl=2020.2=256 19 | - mkl-service=2.3.0=py38h196d8e1_0 20 | - mkl_fft=1.3.0=py38h46781fe_0 21 | - mkl_random=1.1.1=py38h47e9c7a_0 22 | - numpy=1.19.2=py38hadc3359_0 23 | - numpy-base=1.19.2=py38ha3acd2a_0 24 | - openssl=1.1.1j=h8ffe710_0 25 | - pip=21.0.1=py38haa95532_0 26 | - python=3.8.8=hdbf39b2_4 27 | - python_abi=3.8=1_cp38 28 | - pytz=2021.1=pyhd8ed1ab_0 29 | - scikit-learn=0.24.1=py38hf11a4ad_0 30 | - scipy=1.6.1=py38h14eb087_0 31 | - setuptools=52.0.0=py38haa95532_0 32 | - six=1.15.0=py38haa95532_0 33 | - sqlite=3.33.0=h2a8f88b_0 34 | - sqlparse=0.4.1=pyh9f0ad1d_0 35 | - threadpoolctl=2.1.0=pyh5ca1d4c_0 36 | - tzdata=2020f=h52ac0ba_0 37 | - vc=14.2=h21ff451_1 38 | - vs2015_runtime=14.27.29016=h5e58377_2 39 | - wheel=0.36.2=pyhd3eb1b0_0 40 | - wincertstore=0.2=py38_0 41 | - zlib=1.2.11=h62dcd97_4 42 | - pip: 43 | - amqp==5.0.6 44 | - asgiref==3.3.4 45 | - billiard==3.6.4.0 46 | - celery==5.0.5 47 | - cffi==1.14.5 48 | - chardet==4.0.0 49 | - click==7.1.2 50 | - click-didyoumean==0.0.3 51 | - click-plugins==1.1.1 52 | - click-repl==0.1.6 53 | - cryptography==3.4.7 54 | - django==3.2 55 | - django-celery-results==2.0.1 56 | - django-ranged-response==0.2.0 57 | - django-simple-captcha==0.5.14 58 | - dnspython==1.16.0 59 | - eventlet==0.30.2 60 | - greenlet==1.0.0 61 | - humanize==3.4.1 62 | - idna==2.10 63 | - kombu==5.0.2 64 | - librabbitmq==2.0.0 65 | - msgpack==1.0.2 66 | - pillow==8.1.2 67 | - prometheus-client==0.8.0 68 | - prompt-toolkit==3.0.18 69 | - pycparser==2.20 70 | - redis==3.5.3 71 | - requests==2.25.1 72 | - stock==0.1 73 | - tornado==6.1 74 | - urllib3==1.26.4 75 | - vine==5.0.0 76 | - wcwidth==0.2.5 77 | prefix: F:\Anaconda\envs\learn 78 | -------------------------------------------------------------------------------- /utils/scanFile.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | # author: fe1w0 3 | import joblib 4 | from utils.utils import * 5 | 6 | def check(file_name,cv,tf,gnb): 7 | """ 8 | webshell 检测的主要函数 9 | :param file_name: 10 | :param cv: 11 | :param tf: 12 | :param gnb: 13 | :return: 14 | """ 15 | opcode = [load_php_opcode(file_name)] 16 | if opcode == [""]: 17 | return "Error!" 18 | opcode = cv.transform(opcode).toarray() 19 | opcode = tf.transform(opcode).toarray() 20 | 21 | # predict 22 | predict = gnb.predict(opcode)[-1] 23 | 24 | return predict 25 | 26 | def file_check(dir): 27 | """ 28 | 对 dir 文件 进行 webshell 查杀 29 | :param dir: 30 | :return: 31 | """ 32 | # init of CountVectorizer, TfidfTransformer and GaussianNB 33 | cv = joblib.load(r'./TrainedData/cv.pkl') 34 | tf = joblib.load(r'./TrainedData/tf.pkl') 35 | gnb = joblib.load(r'./TrainedData/gnb.pkl') 36 | response_result = [] 37 | result = check(dir, cv, tf, gnb) 38 | if result != "Error!": 39 | response_result.append("{} is WebShell".format(dir)) 40 | elif result == "Error!": 41 | response_result.append("Error!") 42 | else: 43 | response_result.append("{} is not WebShell".format(dir)) 44 | return response_result 45 | 46 | 47 | def folder_check(dir): 48 | """ 49 | 对目录下的文件进行webshell扫描 50 | :param dir: 51 | :return: 52 | """ 53 | response_result = [] 54 | # init of CountVectorizer, TfidfTransformer and GaussianNB 55 | cv = joblib.load(r'./TrainedData/cv.pkl') 56 | tf = joblib.load(r'./TrainedData/tf.pkl') 57 | gnb = joblib.load(r'./TrainedData/gnb.pkl') 58 | 59 | for root, dirs, files in os.walk(dir): 60 | for filename in files: 61 | if filename.endswith('.php') or filename.endswith('.php3') or filename.endswith('.php5') or filename.endswith('.phtml') or filename.endswith('.pht'): 62 | try: 63 | full_path = os.path.join(root, filename) 64 | result = check(full_path, cv, tf, gnb) 65 | if result != "Error!": 66 | response_result.append("{} is WebShell".format(full_path)) 67 | except Exception as e: 68 | print(e) 69 | return response_result 70 | 71 | def webshellScan(php_file_name): 72 | """ 73 | 对文件进行 webshell 扫描 74 | :param php_file_name: 75 | :return: 76 | """ 77 | if os.path.isdir(php_file_name): 78 | return folder_check(php_file_name) 79 | if os.path.isfile(php_file_name): 80 | return file_check(php_file_name) -------------------------------------------------------------------------------- /user/templates/user/forget_index.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 忘记密码 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 |

ScanWebShell

38 | 42 |
43 |
44 | 45 |
46 | 57 |
58 | 59 | 62 |
63 | 64 | 65 | {#刷新验证码的脚本,放到body部分的最后面即可#} 66 | 74 | 75 | -------------------------------------------------------------------------------- /user/templates/user/register.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Register 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |

ScanWebShell

36 | 40 |
41 |
42 | 43 | 44 |
45 | 58 |
59 | 60 | 63 |
64 | 65 | 66 | {#刷新验证码的脚本,放到body部分的最后面即可#} 67 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /user/templates/user/login.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Login 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |

ScanWebShell

37 | 42 |
43 |
44 | 45 | 46 |
47 | 63 |
64 | 65 | 68 |
69 | 70 | 71 | 72 | {#刷新验证码的脚本,放到body部分的最后面即可#} 73 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /user/templates/user/forget_change.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 重置密码 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |

ScanWebShell

37 | 41 |
42 |
43 | 44 |
45 | 76 |
77 | 78 | 79 | 82 | 83 | 84 | {#刷新验证码的脚本,放到body部分的最后面即可#} 85 | 93 |
94 | 95 | -------------------------------------------------------------------------------- /job/templates/job/search.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Search 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |

ScanWebShell

28 | 34 |
35 |
36 | 37 |
38 | 39 | {% if message_warning %} 40 | 41 | {% endif %} 42 | {% if not message_warning %} 43 |

检测文件为:{{ scan_task.scan_file_name }}

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {% for id in range_id %} 53 | 54 | 55 | 56 | 57 | {% endfor %} 58 | 59 |
ID结果
{{ id|add:1 }}{{ contacts_files|get_item:id }}
60 | 61 | 100 | {% endif %} 101 | 102 |
103 | 104 | 107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /ScanWebShell/settings.example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for ScanWebShell project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | import os 15 | 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = '-ngct$xw+vko9m%u7a+ln_1ua)9t2xyf2+w+)2+an8bq#1m0l_' #出于安全角度,强烈建议更换 SECRET_KEY 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = False 29 | 30 | ALLOWED_HOSTS = [ '*' ] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'job', 37 | 'user', 38 | 'captcha', 39 | 'ScanWebShell', 40 | 'django_celery_results', 41 | 'django.contrib.admin', 42 | 'django.contrib.auth', 43 | 'django.contrib.contenttypes', 44 | 'django.contrib.sessions', 45 | 'django.contrib.messages', 46 | 'django.contrib.staticfiles', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'ScanWebShell.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [BASE_DIR / 'templates'] 65 | , 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'ScanWebShell.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'ENGINE': 'django.db.backends.sqlite3', 87 | 'NAME': BASE_DIR / 'db.sqlite3', 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'zh-Hans' 115 | 116 | TIME_ZONE = 'Asia/Shanghai' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 126 | 127 | STATIC_URL = '/static/' 128 | 129 | if DEBUG: 130 | STATICFILES_DIRS = [ 131 | os.path.join(BASE_DIR, 'static') 132 | ] 133 | else: 134 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 135 | 136 | # Default primary key field type 137 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 138 | 139 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 140 | 141 | #QQ邮箱 142 | # 发送邮件配置 143 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 144 | # smpt服务地址 145 | EMAIL_HOST = '' 146 | EMAIL_PORT = 25 # 端口默认都是25不需要修改 147 | # 发送邮件的邮箱,需要配置开通SMTP 148 | EMAIL_HOST_USER = '' 149 | # 在邮箱中设置的客户端授权密码 150 | # 此处的EMAIL_HOST_PASSWORD是用QQ邮箱授权码登录 151 | EMAIL_HOST_PASSWORD = '' 152 | 153 | # 注册有效期天数 154 | CONFIRM_DAYS = 7 155 | 156 | 157 | # CELERY 配置 158 | CELERY_BROKER_URL = 'redis://localhost:6379' 159 | # CELERY_RESULT_BACKEND = 'redis://localhost:6379' 160 | CELERY_RESULT_BACKEND = 'django-db' 161 | CELERY_ACCEPT_CONTENT = ['application/json'] 162 | CELERY_RESULT_SERIALIZER = 'json' 163 | CELERY_TASK_SERIALIZER = 'json' 164 | CELERY_TIMEZONE = 'Asia/Shanghai' 165 | # CELERY_BEAT_SCHEDULE = {} 166 | 167 | 168 | # 图片大小 169 | CAPTCHA_IMAGE_SIZE = (80, 25) -------------------------------------------------------------------------------- /job/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import json 3 | from django.shortcuts import render, redirect 4 | from .forms import UploadFileForm 5 | from .models import ModelWithFileField, ScanTaskField 6 | from user import models 7 | from job.tasks import scanTask 8 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 9 | from django.template.defaulttags import register 10 | 11 | 12 | @register.filter 13 | def get_item(dictionary, key): 14 | return dictionary[key][11:] 15 | 16 | 17 | def upload_file(request): 18 | """ 19 | 上传文件 20 | :param request: 21 | :return: 22 | """ 23 | if not request.session.get('is_login', None): # 不允许重复登录 24 | return redirect('/user/index/') 25 | if request.method == 'POST': 26 | form = UploadFileForm(request.POST, request.FILES) 27 | if form.is_valid(): 28 | user_id = request.session.get('user_id') 29 | if user_id: 30 | tmp_user = models.User.objects.get(id=user_id) 31 | instance = ModelWithFileField(tmp_file=request.FILES['file'], file_user=tmp_user) 32 | instance.save() 33 | 34 | message = "上传成功!\n存储的文件名为:\n" + instance.tmp_file.name 35 | return render(request, 'job/upload.html', {'message_success': message}) 36 | else: 37 | return render(request, 'job/upload.html', {'message_warning': "上传失败"}) 38 | else: 39 | form = UploadFileForm() 40 | return render(request, 'job/upload.html', {'form': form}) 41 | 42 | 43 | def scan_file(request): 44 | """ 45 | 扫描文件,并将任务交给celery处理 46 | :param request: 47 | :return: 48 | """ 49 | if not request.session.get('is_login', None): 50 | return redirect('/user/index/') 51 | user_id = request.session.get('user_id') 52 | if user_id: 53 | tmp_user = models.User.objects.get(id=user_id) 54 | file_name = request.GET.get('file') 55 | 56 | def check(file_name): 57 | """ 58 | 检测是否已经存在相同的任务,有相同的返回True 反之 False 59 | :param file_name: 60 | :return: 61 | """ 62 | try: 63 | model = ScanTaskField.objects.get(scan_file_name=file_name) 64 | return True 65 | except: 66 | return False 67 | 68 | if check(file_name): 69 | message = "已经有相同的扫描任务!" 70 | return render(request, 'job/scan.html', {'message_warning': '已经有相同的扫描任务!'}) 71 | res = scanTask.delay(file_name=file_name) 72 | scantask = ScanTaskField(task_id=res.task_id, task_creator=tmp_user, scan_file_name=file_name) 73 | scantask.save() 74 | message_success = ''' 75 | "Status":"successful","Task_id":{} 76 | '''.format(res.task_id) 77 | return render(request, 'job/scan.html', {'message_success': message_success}) 78 | 79 | 80 | def countResult(request): 81 | """ 82 | 统计该用户的上传文件信息和任务信息,并输出 83 | v1.2版 添加 按钮功能 84 | :param request: 85 | :return: 86 | """ 87 | if not request.session.get('is_login', None): 88 | return redirect('/user/index/') 89 | 90 | user_id = request.session.get('user_id') 91 | if user_id: 92 | current_user = models.User.objects.get(id=user_id) 93 | upload_file_all = current_user.modelwithfilefield_set.all() 94 | scan_task_all = current_user.scantaskfield_set.all() 95 | 96 | paginator_upload_file = Paginator(upload_file_all, 5) # 5 个为一个表 97 | paginator_scan_task = Paginator(scan_task_all, 5) # 5 个为一个表 98 | 99 | page_file = request.GET.get('page_file') 100 | page_task = request.GET.get('page_task') 101 | 102 | try: 103 | contacts_files = paginator_upload_file.page( 104 | page_file 105 | ) 106 | contacts_tasks = paginator_scan_task.page( 107 | page_task 108 | ) 109 | except PageNotAnInteger: 110 | # If page is not an integer, deliver first page. 111 | contacts_files = paginator_upload_file.page(1) 112 | contacts_tasks = paginator_scan_task.page(1) 113 | except EmptyPage: 114 | # If page is out of range (e.g. 9999), deliver last page of results. 115 | contacts_files = paginator_upload_file.page( 116 | paginator_upload_file.num_pages 117 | ) 118 | contacts_tasks = paginator_scan_task.page( 119 | paginator_scan_task.num_pages 120 | ) 121 | try: 122 | page_file = int(page_file) 123 | page_task = int(page_task) 124 | except: 125 | page_file = 1 126 | page_task = 1 127 | return render(request, 'job/count.html', locals()) 128 | 129 | 130 | def searchResult(request): 131 | """ 132 | 根据task_id查看任务具体信息,同时有用户检验 133 | :param request: 134 | :return: 135 | """ 136 | if not request.session.get('is_login', None): 137 | return redirect('/user/index/') 138 | user_id = request.session.get('user_id') 139 | task_id = request.GET.get('task_id') 140 | scan_task = ScanTaskField.objects.get(task_id=task_id) 141 | print(scan_task.task_status) 142 | if scan_task.task_status != "SUCCESS": 143 | return render(request, 'job/search.html', {'message_warning': '未完成!'}) 144 | if user_id == scan_task.task_creator.id: # 此时放回 |文件名|任务创建时间|结果| 145 | try: 146 | scan_task_result = scan_task.task_result.result # 结果类型为 array 经过 json序列化后的值 147 | except: 148 | return render(request, 'job/search.html', {'message_warning': '未完成!'}) 149 | scan_task_result_array = json.loads(json.loads(scan_task_result)) 150 | 151 | paginator_result = Paginator(scan_task_result_array, 15) 152 | page = request.GET.get('page') 153 | try: 154 | contacts_files = paginator_result.page( 155 | page 156 | ) 157 | except PageNotAnInteger: 158 | contacts_files = paginator_result.page( 159 | 1 160 | ) 161 | except EmptyPage: 162 | contacts_files = paginator_result.page( 163 | paginator_result.num_pages 164 | ) 165 | try: 166 | page = int(page) 167 | except: 168 | page = 1 169 | range_id = range(len(contacts_files.object_list)) 170 | return render(request, 'job/search.html', locals()) 171 | -------------------------------------------------------------------------------- /job/templates/job/count.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Count 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |

ScanWebShell

28 | 33 |
34 |
35 | 36 |
37 | 38 |

文件表格

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for file in contacts_files %} 49 | 50 | 51 | 52 | 53 | 54 | 55 | {% endfor %} 56 | 57 |
上传时间文件名操作
{{ file.c_time |date:"Y-m-d H:i:s" }}{{ file.tmp_file }}扫描
58 | 59 | 98 | 99 |

任务表格

100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {% for task in contacts_tasks %} 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | {% endfor %} 120 | 121 |
任务ID时间文件名结果操作
{{ task.task_id }}{{ task.c_time |date:"Y-m-d H:i:s" }}{{ task.scan_file_name }}{{ task.task_status }}详情
122 | 123 | 162 | 163 |
164 | 165 | 168 |
169 | 170 | 171 | -------------------------------------------------------------------------------- /开发文档.md: -------------------------------------------------------------------------------- 1 | # ScanWebShell 开发文档 2 | 3 | [toc] 4 | 5 | 6 | 7 | 8 | 9 | # 总体设计 10 | 11 | 后端设计如下: 12 | 13 | 14 | 15 | | URL | 视图 | 模板 | 说明 | 16 | | -------------------- | ------------------------ | ------------------ | ------------------------------ | 17 | | /index/ | ScanWebShell.views.index | user/index.html | 主页与项目信息 | 18 | | /user/login/ | user.views.login | user/login.html | 登录 | 19 | | /user/register/ | user.views.register | user/register.html | 注册 | 20 | | /user/logout/ | user.views.logout | 无需专门的页面 | 登出 | 21 | | /job/upload | job.views.upload_file | job/upload.html | 上传文件 | 22 | | /job/count | job.views.countResult | job/count.html | 显示所有任务信息 | 23 | | /job/search/?task_id | job.views.searchResult | job/search.html | 根据task_id 返回任务的详细信息 | 24 | | /job/scan?file | job.views.scan_file | job/scan.html | 扫描文件 | 25 | 26 | * index 项目信息 27 | * user 与用户相关的处理 28 | * login 登录 29 | * register 登录 30 | * forget 忘记密码 31 | * logut 登出 32 | * job 任务相关处理 33 | * upload 上传webshell文件 34 | * scan 对上传的文件进行扫描 35 | * count 统计当前用户所有的扫描任务与已上传的文件 36 | * search 具体描述某一任务,重点为改任务的结果 37 | 38 | 其中,仅`index`允许游客访问。 39 | > 环境为开发环境,上传的是生产环境 40 | # User 应用设计 41 | 42 | ## 后端设计 43 | 44 | ### 参考的项目 45 | 46 | User 其MVT中的`Module`和`view`部分,参考于[基于Django2.2可重用登录与注册系统](https://www.liujiangblog.com/course/django/102) 47 | 48 | 模型如下: 49 | 50 | ```python 51 | class User(models.Model): 52 | """ 53 | 用户模型 54 | """ 55 | name = models.CharField(max_length=128,unique=True) 56 | password = models.CharField(max_length=256) 57 | email = models.EmailField(unique=True) 58 | c_time = models.DateTimeField(auto_now_add=True) 59 | has_confirmed = models.BooleanField(default=False) 60 | 61 | def __str__(self): 62 | return self.name 63 | 64 | class Meta: 65 | ordering = ["-c_time"] 66 | verbose_name = "用户" 67 | verbose_name_plural = "用户" 68 | 69 | 70 | class ConfirmString(models.Model): 71 | """ 72 | 邮箱确认模型 73 | """ 74 | code = models.CharField(max_length=256) 75 | user = models.OneToOneField('User', on_delete=models.CASCADE) 76 | c_time = models.DateTimeField(auto_now_add=True) 77 | 78 | def __str__(self): 79 | return self.user.name + ": " + self.code 80 | 81 | class Meta: 82 | 83 | ordering = ["-c_time"] 84 | verbose_name = "确认码" 85 | verbose_name_plural = "确认码" 86 | ``` 87 | 88 | ### 其他功能 89 | 90 | 之后的`View`部分是在[基于Django2.2可重用登录与注册系统](https://www.liujiangblog.com/course/django/102)的基础上,补充部分功能: 91 | 92 | * 忘记密码 93 | * 重置密码 94 | * django simple captcha refresh 95 | 96 | #### 忘记密码 97 | 98 | 其中重置密码没有独立出来,是属于忘记密码的一部分 99 | 100 | 相关模型如下: 101 | 102 | ```python 103 | class ConfirmString(models.Model): 104 | """ 105 | 邮箱确认模型 106 | """ 107 | code = models.CharField(max_length=256) 108 | user = models.OneToOneField('User', on_delete=models.CASCADE) 109 | c_time = models.DateTimeField(auto_now_add=True) 110 | 111 | def __str__(self): 112 | return self.user.name + ": " + self.code 113 | 114 | class Meta: 115 | 116 | ordering = ["-c_time"] 117 | verbose_name = "确认码" 118 | verbose_name_plural = "确认码" 119 | ``` 120 | 121 | 应用逻辑如下: 122 | 123 | * 用户在`user/forget/index`的表单中,添加需要重置密码的用户邮箱 124 | * 若无改用户,则弹出无该用户的警告 125 | * 有该邮箱,则往用户邮箱发送重置密码的链接,此时 126 | * 重置密码的链接大致为`user/forget/confirm/?code=*` 127 | * 当code是在`ConfirmString`实例中时,将`user`的`has_confirmed`,使其在重置密码期间无法登录,之后携带`code`转到`user/forget/change/?code=*` 128 | * 若数据库中没有该`code`,则拒绝 129 | * `user/forget/change/?code=*`中,根据`code`查询一对一匹配的`user`,再根据添加的表单修改密码,之后`confirm.user.save()`和`confirm.delete()` 130 | 131 | #### django simple captcha refresh 132 | 133 | 在原项目基础上,需要修改`Template`和`urls.py` 134 | 135 | * `urls.py` 136 | 137 | captcha.views 内置就有刷新验证码的方法 138 | 139 | ```python 140 | from captcha.views import captcha_refresh # 验证码刷新功能,captcha_refresh为captcha.views内置方法,不需要我们单独写 141 | 142 | urlpatterns = [ 143 | ... 144 | path('refresh/', captcha_refresh), # 点击可以刷新验证码 145 | 146 | ] 147 | ``` 148 | 149 | * `Template` 150 | 151 | ```html 152 | {#刷新验证码的脚本,放到body部分的最后面即可#} 153 | 161 | ``` 162 | 163 | ## 前端设计 164 | 165 | 前端设计上是基本参考于[bootstrapdoc 5.0 example](https://bootstrapdoc.com/docs/5.0/example/). 166 | 167 | * index.html 168 | 169 | ![image-20210421173150850](http://img.xzaslxr.xyz/image-20210421173150850.png) 170 | 171 | * login.html 172 | 173 | ![image-20210421173225760](http://img.xzaslxr.xyz/image-20210421173225760.png) 174 | 175 | # Job 应用设计 176 | 177 | ## 后端设计 178 | 179 | `Job`的应用设计上,个人在设计时,分为一些几个功能: 180 | 181 | * Upload 上传WebShell 文件 182 | * Count 统计当前用户的上传文件和扫描任务 183 | * Scan 根据`file`文件创建扫描任务 184 | * Search 根据`task_id`查询扫描任务结果 185 | 186 | 在四个任务中,`upload`、`count`、`search`设计相对简单,网上参考也相对较多,这里只是简单介绍。而`scan`中的设计相对麻烦,本质上是利用`celery`来处理扫描任务。 187 | 188 | ### upload 189 | 190 | 在`models`中设计相关模型,且添加装饰器,用于在`admin`可以方便地同时删除文件对象和磁盘中的文件。 191 | 192 | * `models.py` 193 | 194 | ```python 195 | class ModelWithFileField(models.Model): 196 | tmp_file = models.FileField(upload_to = './FileUpload/')# 上传目录为 FileUpload 197 | file_user = models.ForeignKey(User, on_delete=models.CASCADE,null=True) 198 | ''' 199 | 值得注意的一点是,FileUpload中已经存在相同文件名的文件时,会对上传文件的文件名重命名 200 | 如 1.png 转为 1_fIZVhN3.png 201 | 且存储的文件为 1_fIZVhN3.png 202 | ''' 203 | c_time = models.DateTimeField(auto_now_add=True) 204 | 205 | def __str__(self): 206 | return self.tmp_file.name 207 | 208 | class Meta: 209 | ordering = ["-c_time"] 210 | verbose_name = "文件" 211 | verbose_name_plural = "文件" 212 | 213 | # 添加装饰器 214 | @receiver(post_delete,sender=ModelWithFileField) 215 | def delete_upload_files(sender, instance, **kwargs): 216 | files = getattr(instance, 'tmp_file') 217 | if not files: 218 | return 219 | fname = os.path.join(settings.MEDIA_ROOT, str(files)) 220 | if os.path.isfile(fname): 221 | os.remove(fname) 222 | ``` 223 | 224 | * `views.py` 225 | 226 | ```python 227 | def upload_file(request): 228 | """ 229 | 上传文件 230 | :param request: 231 | :return: 232 | """ 233 | if not request.session.get('is_login', None): # 不允许重复登录 234 | return redirect('/user/index/') 235 | if request.method == 'POST': 236 | form = UploadFileForm(request.POST, request.FILES) 237 | if form.is_valid(): 238 | user_id = request.session.get('user_id') 239 | if user_id: 240 | tmp_user = models.User.objects.get(id=user_id) 241 | instance = ModelWithFileField(tmp_file=request.FILES['file'], file_user=tmp_user) 242 | instance.save() 243 | 244 | message = "上传成功!\n存储的文件名为:\n" + instance.tmp_file.name 245 | return render(request, 'job/upload.html', {'message_success': message}) 246 | else: 247 | return render(request, 'job/upload.html', {'message_warning': "上传失败"}) 248 | else: 249 | form = UploadFileForm() 250 | return render(request, 'job/upload.html', {'form': form}) 251 | ``` 252 | 253 | ### scan 254 | 255 | scan设计思路如下: 256 | 257 | 1. 利用`celery`和`redis`,作为任务调度模块 258 | 2. 当scan成功访问,`file`文件存在和无相关任务时,后台分别创建`ScanTaskField`实例和启动`celery`中的 `scanTask.delay(file_name=file_name)` 259 | 3. 当`celery`中任务完成,自动更新`ScanTaskField`实例(同样需要添加装饰器) 260 | 261 | 参考文档[docs.celeryproject.org/en/v5.0.5/django/first-steps-with-django](https://docs.celeryproject.org/en/v5.0.5/django/first-steps-with-django.html?highlight=django#first-steps-with-django) 262 | 263 | ## 前端设计 264 | 265 | 前端设计上同样是基本参考于[bootstrapdoc 5.0 example](https://bootstrapdoc.com/docs/5.0/example/). 266 | 267 | * `job/count`![image-20210421180506546](http://img.xzaslxr.xyz/image-20210421180506546.png) 268 | 269 | * `job/upload`![image-20210421180545851](http://img.xzaslxr.xyz/image-20210421180545851.png) 270 | 271 | # 应用配置 272 | 273 | 邮箱功能需要在`settings.py`中配置如下参数: 274 | 275 | ![image-20210421180717445](http://img.xzaslxr.xyz/image-20210421180717445.png) 276 | 277 | 在`celery`中设置`worker`为`redis`,需要 278 | ```bash 279 | `docker run --name=redis -d -p 6379:6379 redis` 280 | ``` 281 | 282 | `celery`启动 283 | ```bash 284 | celery -A ScanWebShell worker -l info 285 | ``` 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | ​ -------------------------------------------------------------------------------- /user/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.shortcuts import redirect 3 | from . import models 4 | from . import forms 5 | import hashlib 6 | import datetime 7 | from django.conf import settings 8 | import pytz 9 | 10 | 11 | def send_forget_email(email, code): 12 | """ 13 | 发送 忘记密码确认邮件 14 | :param email: 15 | :param code: 16 | :return: 17 | """ 18 | from django.core.mail import EmailMultiAlternatives 19 | 20 | subject = '来自site.com的忘记密码确认邮件' 21 | 22 | text_content = '''该邮件为忘记密码确认邮件!\ 23 | 如果你看到这条消息,说明你的邮箱服务器不提供HTML链接功能,请联系管理员!''' 24 | 25 | html_content = ''' 26 |

该链接为忘记密码确认链接,

27 |

请点击站点链接确认从而重置密码!

28 |

此链接有效期为{}天!

29 | '''.format('127.0.0.1:8000', code, settings.CONFIRM_DAYS) 30 | 31 | msg = EmailMultiAlternatives(subject, text_content, settings.EMAIL_HOST_USER, [email]) 32 | msg.attach_alternative(html_content, "text/html") 33 | msg.send() 34 | 35 | 36 | def send_email(email, code): 37 | """ 38 | 注册确认邮件 39 | :param email: 40 | :param code: 41 | :return: 42 | """ 43 | from django.core.mail import EmailMultiAlternatives 44 | 45 | subject = '来自site.com的注册确认邮件' 46 | 47 | text_content = '''感谢注册site.com,这里是WebShell查杀平台!\ 48 | 如果你看到这条消息,说明你的邮箱服务器不提供HTML链接功能,请联系管理员!''' 49 | 50 | html_content = ''' 51 |

感谢注册注册链接,\ 52 | 这里是WebShell查杀平台!

53 |

请点击站点链接完成注册确认!

54 |

此链接有效期为{}天!

55 | '''.format('127.0.0.1:8000', code, settings.CONFIRM_DAYS) 56 | 57 | msg = EmailMultiAlternatives(subject, text_content, settings.EMAIL_HOST_USER, [email]) 58 | msg.attach_alternative(html_content, "text/html") 59 | msg.send() 60 | 61 | 62 | def make_confirm_string(user): 63 | """ 64 | 依据user,注册ConfirmString模型,并返回code 65 | :param user: 66 | :return: 67 | """ 68 | now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 69 | code = hash_code(user.name, now) 70 | models.ConfirmString.objects.create(code=code, user=user, ) 71 | return code 72 | 73 | 74 | def hash_code(s, salt="mysite"): 75 | h = hashlib.sha256() 76 | s += salt 77 | h.update(s.encode()) 78 | return h.hexdigest() 79 | 80 | 81 | def index(request): 82 | """ 83 | 首页 84 | :param request: 85 | :return: 86 | """ 87 | if not request.session.get('is_login', None): # 未登录情况下 88 | return redirect('/user/login/') 89 | return redirect('/index/') 90 | 91 | 92 | def login(request): 93 | """ 94 | 登录 95 | :param request: 96 | :return: 97 | """ 98 | if request.session.get('is_login', None): # 根据 session 来检测是否登录 99 | return redirect('/index/') # 若已经登录,跳转到index 页面 100 | if request.method == 'POST': 101 | login_form = forms.UserForm(request.POST) 102 | message = '请检查填写的内容!' 103 | if login_form.is_valid(): 104 | username = login_form.cleaned_data.get('username') 105 | password = login_form.cleaned_data.get('password') 106 | 107 | try: 108 | user = models.User.objects.get(name=username) 109 | except: 110 | message = '用户不存在!' 111 | return render(request, 'user/login.html', locals()) 112 | if not user.has_confirmed: 113 | message = '该用户还未经过邮件确认!' 114 | return render(request, 'user/login.html', locals()) 115 | 116 | if user.password == hash_code(password): 117 | ''' 118 | 注意此处session的存储,在整个处理过程中很重要 119 | ''' 120 | request.session['is_login'] = True 121 | request.session['user_id'] = user.id 122 | request.session['user_name'] = user.name 123 | return redirect('/index/') 124 | else: 125 | message = '密码不正确!' 126 | return render(request, 'user/login.html', locals()) 127 | 128 | else: 129 | return render(request, 'user/login.html', locals()) 130 | 131 | login_form = forms.UserForm() 132 | return render(request, 'user/login.html', locals()) 133 | 134 | 135 | def register(request): 136 | """ 137 | 注册 138 | :param request: 139 | :return: 140 | """ 141 | if request.session.get('is_login', None): 142 | return redirect('/index/') 143 | 144 | if request.method == 'POST': 145 | register_form = forms.RegisterForm(request.POST) 146 | message = "请检查填写的内容!" 147 | if register_form.is_valid(): 148 | username = register_form.cleaned_data.get('username') 149 | password1 = register_form.cleaned_data.get('password1') 150 | password2 = register_form.cleaned_data.get('password2') 151 | email = register_form.cleaned_data.get('email') 152 | 153 | if password1 != password2: 154 | message = '两次输入的密码不同!' 155 | return render(request, 'user/register.html', locals()) 156 | else: 157 | same_name_user = models.User.objects.filter(name=username) 158 | if same_name_user: 159 | message = '用户名已经存在' 160 | return render(request, 'user/register.html', locals()) 161 | same_email_user = models.User.objects.filter(email=email) 162 | if same_email_user: 163 | message = '该邮箱已经被注册了!' 164 | return render(request, 'user/register.html', locals()) 165 | 166 | new_user = models.User() 167 | new_user.name = username 168 | new_user.password = hash_code(password1) 169 | new_user.email = email 170 | new_user.save() 171 | 172 | code = make_confirm_string(new_user) 173 | send_email(email, code) 174 | 175 | message = '请前往邮箱进行确认!' 176 | return render(request, 'user/confirm.html', locals()) 177 | else: 178 | return render(request, 'user/register.html', locals()) 179 | register_form = forms.RegisterForm() 180 | return render(request, 'user/register.html', locals()) 181 | 182 | 183 | def logout(request): 184 | """ 185 | 登出 186 | :param request: 187 | :return: 188 | """ 189 | if not request.session.get('is_login', None): # 未登录,先转到 user/login 190 | return redirect("/user/login") 191 | request.session.flush() # 已登录,也要转到 user/login 192 | return redirect("/user/login/") 193 | 194 | 195 | def user_confirm(request): 196 | """ 197 | 邮箱确认 198 | :param request: 199 | :return: 200 | """ 201 | code = request.GET.get('code', None) 202 | try: 203 | confirm = models.ConfirmString.objects.get(code=code) 204 | except: 205 | message_warning = '无效的确认请求!' 206 | return render(request, 'user/confirm.html', locals()) 207 | 208 | c_time = confirm.c_time 209 | now = datetime.datetime.now() 210 | now = now.replace(tzinfo=pytz.timezone('UTC')) 211 | if now > c_time + datetime.timedelta(settings.CONFIRM_DAYS): 212 | confirm.user.delete() # 删除用户,可以影响到关联的User 213 | message_warning = '您的邮件已经过期!请重新注册!' 214 | return render(request, 'user/confirm.html', locals()) 215 | else: 216 | confirm.user.has_confirmed = True 217 | confirm.user.save() 218 | confirm.delete() 219 | message_success = '感谢确认,请使用账户登录!' 220 | return render(request, 'user/confirm.html', locals()) 221 | 222 | 223 | def forget_index(request): 224 | """ 225 | 忘记密码视图 226 | :param request: 227 | :return: 228 | """ 229 | if request.method == 'POST': 230 | forget_index_form = forms.ForgetIndexForm(request.POST) 231 | 232 | if forget_index_form.is_valid(): 233 | email = forget_index_form.cleaned_data.get('email') 234 | # same_email_user = models.User.objects.filter(email=email) 235 | same_email_user = models.User.objects.get(email=email) 236 | if not same_email_user: 237 | message = "不存在使用该邮箱的用户!" 238 | return render(request, 'user/forget_index.html', locals()) 239 | else: 240 | code = make_confirm_string(same_email_user) 241 | send_forget_email(email, code) 242 | message = '请前往邮箱进行确认!' 243 | return render(request, 'user/confirm.html', locals()) 244 | else: 245 | message = "请检查填写的内容!" 246 | return render(request, 'user/forget_index.html', locals()) 247 | forget_index_form = forms.ForgetIndexForm() 248 | return render(request, 'user/forget_index.html', locals()) 249 | 250 | 251 | def forget_confirm(request): 252 | """ 253 | 忘记密码邮箱确认 254 | :param request: 255 | :return: 256 | """ 257 | code = request.GET.get('code', None) 258 | message = '' 259 | try: 260 | confirm = models.ConfirmString.objects.get(code=code) 261 | except: 262 | message = '无效的确认请求!' 263 | return render(request, 'user/forget_confirm.html', locals()) 264 | 265 | c_time = confirm.c_time 266 | now = datetime.datetime.now() 267 | now = now.replace(tzinfo=pytz.timezone('UTC')) 268 | if now > c_time + datetime.timedelta(settings.CONFIRM_DAYS): 269 | confirm.user.clear() 270 | confirm.delete() 271 | message = '您的邮件已经过期!请重新使用忘记密码功能!' 272 | return render(request, 'user/forget_confirm.html', locals()) 273 | else: 274 | confirm.user.has_confirmed = False 275 | confirm.user.save() 276 | return redirect('/user/forget/change/?code=' + str(code)) 277 | 278 | 279 | def forget_change(request): 280 | """ 281 | 进行修改密码 282 | :param request: 283 | :return: 284 | """ 285 | code = request.GET.get('code', None) 286 | if code: 287 | request.session['code'] = code 288 | if request.method == 'POST': 289 | forget_change_form = forms.ForgetChangeForm(request.POST) 290 | if forget_change_form.is_valid(): 291 | password1 = forget_change_form.cleaned_data.get('password1') 292 | password2 = forget_change_form.cleaned_data.get('password2') 293 | code = forget_change_form.cleaned_data.get('code') 294 | try: 295 | confirm = models.ConfirmString.objects.get(code=code) 296 | except: 297 | message = '无效重置密码请求!' 298 | return render(request, 'user/forget_change.html', locals()) 299 | message = '两次输入的密码不同!' 300 | if password1 != password2: 301 | return render(request, 'user/forget_change.html', locals()) 302 | confirm.user.password = hash_code(password1) 303 | confirm.user.has_confirmed = True 304 | confirm.user.save() 305 | confirm.delete() 306 | message = '密码重置成功!' 307 | request.session.flush() 308 | return render(request, 'user/forget_change.html', locals()) 309 | else: 310 | message = '请检查填写的内容!' 311 | return render(request, 'user/forget_change.html', locals()) 312 | forget_change_form = forms.ForgetChangeForm() 313 | return render(request, 'user/forget_change.html', locals()) 314 | -------------------------------------------------------------------------------- /job/static/job/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.0.0-beta1 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){if(t&&t.__esModule)return t;var e=Object.create(null);return t&&Object.keys(t).forEach((function(n){if("default"!==n){var i=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,i.get?i:{enumerable:!0,get:function(){return t[n]}})}})),e.default=t,Object.freeze(e)}var n=e(t);function i(t,e){for(var n=0;n0,i._pointerEvent=Boolean(window.PointerEvent),i._addEventListeners(),i}r(e,t);var n=e.prototype;return n.next=function(){this._isSliding||this._slide("next")},n.nextWhenVisible=function(){!document.hidden&&v(this._element)&&this.next()},n.prev=function(){this._isSliding||this._slide("prev")},n.pause=function(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(p(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},n.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},n.to=function(t){var e=this;this._activeElement=V.findOne(".active.carousel-item",this._element);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)Q.one(this._element,"slid.bs.carousel",(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var i=t>n?"next":"prev";this._slide(i,this._items[t])}},n.dispose=function(){t.prototype.dispose.call(this),Q.off(this._element,G),this._items=null,this._config=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},n._getConfig=function(t){return t=s({},Z,t),_($,t,J),t},n._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},n._addEventListeners=function(){var t=this;this._config.keyboard&&Q.on(this._element,"keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&(Q.on(this._element,"mouseenter.bs.carousel",(function(e){return t.pause(e)})),Q.on(this._element,"mouseleave.bs.carousel",(function(e){return t.cycle(e)}))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()},n._addTouchEventListeners=function(){var t=this,e=function(e){t._pointerEvent&&tt[e.pointerType.toUpperCase()]?t.touchStartX=e.clientX:t._pointerEvent||(t.touchStartX=e.touches[0].clientX)},n=function(e){t._pointerEvent&&tt[e.pointerType.toUpperCase()]&&(t.touchDeltaX=e.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};V.find(".carousel-item img",this._element).forEach((function(t){Q.on(t,"dragstart.bs.carousel",(function(t){return t.preventDefault()}))})),this._pointerEvent?(Q.on(this._element,"pointerdown.bs.carousel",(function(t){return e(t)})),Q.on(this._element,"pointerup.bs.carousel",(function(t){return n(t)})),this._element.classList.add("pointer-event")):(Q.on(this._element,"touchstart.bs.carousel",(function(t){return e(t)})),Q.on(this._element,"touchmove.bs.carousel",(function(e){return function(e){e.touches&&e.touches.length>1?t.touchDeltaX=0:t.touchDeltaX=e.touches[0].clientX-t.touchStartX}(e)})),Q.on(this._element,"touchend.bs.carousel",(function(t){return n(t)})))},n._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.key){case"ArrowLeft":t.preventDefault(),this.prev();break;case"ArrowRight":t.preventDefault(),this.next()}},n._getItemIndex=function(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)},n._getItemByDirection=function(t,e){var n="next"===t,i="prev"===t,o=this._getItemIndex(e),s=this._items.length-1;if((i&&0===o||n&&o===s)&&!this._config.wrap)return e;var r=(o+("prev"===t?-1:1))%this._items.length;return-1===r?this._items[this._items.length-1]:this._items[r]},n._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(V.findOne(".active.carousel-item",this._element));return Q.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:i,to:n})},n._setActiveIndicatorElement=function(t){if(this._indicatorsElement){for(var e=V.find(".active",this._indicatorsElement),n=0;n0)for(var i=0;i0&&s--,"ArrowDown"===t.key&&sdocument.documentElement.clientHeight;e||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");var n=h(this._dialog);Q.off(this._element,"transitionend"),Q.one(this._element,"transitionend",(function(){t._element.classList.remove("modal-static"),e||(Q.one(t._element,"transitionend",(function(){t._element.style.overflowY=""})),m(t._element,n))})),m(this._element,n),this._element.focus()}},n._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;(!this._isBodyOverflowing&&t&&!T||this._isBodyOverflowing&&!t&&T)&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),(this._isBodyOverflowing&&!t&&!T||!this._isBodyOverflowing&&t&&T)&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},n._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},n._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",container:!1,fallbackPlacements:null,boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:Tt,popperConfig:null},Ot={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},It=function(e){function i(t,i){var o;if(void 0===n)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");return(o=e.call(this,t)||this)._isEnabled=!0,o._timeout=0,o._hoverState="",o._activeTrigger={},o._popper=null,o.config=o._getConfig(i),o.tip=null,o._setListeners(),o}r(i,e);var a=i.prototype;return a.enable=function(){this._isEnabled=!0},a.disable=function(){this._isEnabled=!1},a.toggleEnabled=function(){this._isEnabled=!this._isEnabled},a.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=L(t.delegateTarget,e);n||(n=new this.constructor(t.delegateTarget,this._getDelegateConfig()),A(t.delegateTarget,e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}},a.dispose=function(){clearTimeout(this._timeout),Q.off(this._element,this.constructor.EVENT_KEY),Q.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.parentNode.removeChild(this.tip),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.config=null,this.tip=null,e.prototype.dispose.call(this)},a.show=function(){var e=this;if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(this.isWithContent()&&this._isEnabled){var n=Q.trigger(this._element,this.constructor.Event.SHOW),i=function t(e){if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){var n=e.getRootNode();return n instanceof ShadowRoot?n:null}return e instanceof ShadowRoot?e:e.parentNode?t(e.parentNode):null}(this._element),o=null===i?this._element.ownerDocument.documentElement.contains(this._element):i.contains(this._element);if(n.defaultPrevented||!o)return;var s=this.getTipElement(),r=c(this.constructor.NAME);s.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&s.classList.add("fade");var a="function"==typeof this.config.placement?this.config.placement.call(this,s,this._element):this.config.placement,l=this._getAttachment(a);this._addAttachmentClass(l);var u=this._getContainer();A(s,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||u.appendChild(s),Q.trigger(this._element,this.constructor.Event.INSERTED),this._popper=t.createPopper(this._element,s,this._getPopperConfig(l)),s.classList.add("show");var d,f,p="function"==typeof this.config.customClass?this.config.customClass():this.config.customClass;if(p)(d=s.classList).add.apply(d,p.split(" "));if("ontouchstart"in document.documentElement)(f=[]).concat.apply(f,document.body.children).forEach((function(t){Q.on(t,"mouseover",(function(){}))}));var g=function(){var t=e._hoverState;e._hoverState=null,Q.trigger(e._element,e.constructor.Event.SHOWN),"out"===t&&e._leave(null,e)};if(this.tip.classList.contains("fade")){var _=h(this.tip);Q.one(this.tip,"transitionend",g),m(this.tip,_)}else g()}},a.hide=function(){var t=this;if(this._popper){var e=this.getTipElement(),n=function(){"show"!==t._hoverState&&e.parentNode&&e.parentNode.removeChild(e),t._cleanTipClass(),t._element.removeAttribute("aria-describedby"),Q.trigger(t._element,t.constructor.Event.HIDDEN),t._popper&&(t._popper.destroy(),t._popper=null)};if(!Q.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented){var i;if(e.classList.remove("show"),"ontouchstart"in document.documentElement)(i=[]).concat.apply(i,document.body.children).forEach((function(t){return Q.off(t,"mouseover",b)}));if(this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this.tip.classList.contains("fade")){var o=h(e);Q.one(e,"transitionend",n),m(e,o)}else n();this._hoverState=""}}},a.update=function(){null!==this._popper&&this._popper.update()},a.isWithContent=function(){return Boolean(this.getTitle())},a.getTipElement=function(){if(this.tip)return this.tip;var t=document.createElement("div");return t.innerHTML=this.config.template,this.tip=t.children[0],this.tip},a.setContent=function(){var t=this.getTipElement();this.setElementContent(V.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")},a.setElementContent=function(t,e){if(null!==t)return"object"==typeof e&&g(e)?(e.jquery&&(e=e[0]),void(this.config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this.config.html?(this.config.sanitize&&(e=kt(e,this.config.allowList,this.config.sanitizeFn)),t.innerHTML=e):t.textContent=e)},a.getTitle=function(){var t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this._element):this.config.title),t},a.updateAttachment=function(t){return"right"===t?"end":"left"===t?"start":t},a._getPopperConfig=function(t){var e=this,n={name:"flip",options:{altBoundary:!0}};return this.config.fallbackPlacements&&(n.options.fallbackPlacements=this.config.fallbackPlacements),s({},{placement:t,modifiers:[n,{name:"preventOverflow",options:{rootBoundary:this.config.boundary}},{name:"arrow",options:{element:"."+this.constructor.NAME+"-arrow"}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:function(t){return e._handlePopperPlacementChange(t)}}],onFirstUpdate:function(t){t.options.placement!==t.placement&&e._handlePopperPlacementChange(t)}},this.config.popperConfig)},a._addAttachmentClass=function(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))},a._getContainer=function(){return!1===this.config.container?document.body:g(this.config.container)?this.config.container:V.findOne(this.config.container)},a._getAttachment=function(t){return St[t.toUpperCase()]},a._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)Q.on(t._element,t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==e){var n="hover"===e?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i="hover"===e?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;Q.on(t._element,n,t.config.selector,(function(e){return t._enter(e)})),Q.on(t._element,i,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t._element&&t.hide()},Q.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=s({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},a._fixTitle=function(){var t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))},a._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||L(t.delegateTarget,n))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),A(t.delegateTarget,n,e)),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){"show"===e._hoverState&&e.show()}),e.config.delay.show):e.show())},a._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||L(t.delegateTarget,n))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),A(t.delegateTarget,n,e)),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){"out"===e._hoverState&&e.hide()}),e.config.delay.hide):e.hide())},a._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},a._getConfig=function(t){var e=q.getDataAttributes(this._element);return Object.keys(e).forEach((function(t){Ct.has(t)&&delete e[t]})),t&&"object"==typeof t.container&&t.container.jquery&&(t.container=t.container[0]),"number"==typeof(t=s({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_(At,t,this.constructor.DefaultType),t.sanitize&&(t.template=kt(t.template,t.allowList,t.sanitizeFn)),t},a._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},a._cleanTipClass=function(){var t=this.getTipElement(),e=t.getAttribute("class").match(Lt);null!==e&&e.length>0&&e.map((function(t){return t.trim()})).forEach((function(e){return t.classList.remove(e)}))},a._handlePopperPlacementChange=function(t){var e=t.state;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))},i.jQueryInterface=function(t){return this.each((function(){var e=L(this,"bs.tooltip"),n="object"==typeof t&&t;if((e||!/dispose|hide/.test(t))&&(e||(e=new i(this,n)),"string"==typeof t)){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},o(i,null,[{key:"Default",get:function(){return Nt}},{key:"NAME",get:function(){return At}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return Ot}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Dt}}]),i}(U);E((function(){var t=w();if(t){var e=t.fn[At];t.fn[At]=It.jQueryInterface,t.fn[At].Constructor=It,t.fn[At].noConflict=function(){return t.fn[At]=e,It.jQueryInterface}}}));var jt="popover",Pt=new RegExp("(^|\\s)bs-popover\\S+","g"),xt=s({},It.Default,{placement:"right",trigger:"click",content:"",template:''}),Ht=s({},It.DefaultType,{content:"(string|element|function)"}),Bt={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},Mt=function(t){function e(){return t.apply(this,arguments)||this}r(e,t);var n=e.prototype;return n.isWithContent=function(){return this.getTitle()||this._getContent()},n.setContent=function(){var t=this.getTipElement();this.setElementContent(V.findOne(".popover-header",t),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(V.findOne(".popover-body",t),e),t.classList.remove("fade","show")},n._addAttachmentClass=function(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))},n._getContent=function(){return this._element.getAttribute("data-bs-content")||this.config.content},n._cleanTipClass=function(){var t=this.getTipElement(),e=t.getAttribute("class").match(Pt);null!==e&&e.length>0&&e.map((function(t){return t.trim()})).forEach((function(e){return t.classList.remove(e)}))},e.jQueryInterface=function(t){return this.each((function(){var n=L(this,"bs.popover"),i="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new e(this,i),A(this,"bs.popover",n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},o(e,null,[{key:"Default",get:function(){return xt}},{key:"NAME",get:function(){return jt}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return Bt}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Ht}}]),e}(It);E((function(){var t=w();if(t){var e=t.fn[jt];t.fn[jt]=Mt.jQueryInterface,t.fn[jt].Constructor=Mt,t.fn[jt].noConflict=function(){return t.fn[jt]=e,Mt.jQueryInterface}}}));var Rt="scrollspy",Kt={offset:10,method:"auto",target:""},Qt={offset:"number",method:"string",target:"(string|element)"},Ut=function(t){function e(e,n){var i;return(i=t.call(this,e)||this)._scrollElement="BODY"===e.tagName?window:e,i._config=i._getConfig(n),i._selector=i._config.target+" .nav-link, "+i._config.target+" .list-group-item, "+i._config.target+" .dropdown-item",i._offsets=[],i._targets=[],i._activeTarget=null,i._scrollHeight=0,Q.on(i._scrollElement,"scroll.bs.scrollspy",(function(t){return i._process(t)})),i.refresh(),i._process(),i}r(e,t);var n=e.prototype;return n.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?"offset":"position",n="auto"===this._config.method?e:this._config.method,i="position"===n?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(this._selector).map((function(t){var e=d(t),o=e?V.findOne(e):null;if(o){var s=o.getBoundingClientRect();if(s.width||s.height)return[q[n](o).top+i,e]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},n.dispose=function(){t.prototype.dispose.call(this),Q.off(this._scrollElement,".bs.scrollspy"),this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},n._getConfig=function(t){if("string"!=typeof(t=s({},Kt,"object"==typeof t&&t?t:{})).target&&g(t.target)){var e=t.target.id;e||(e=c(Rt),t.target.id=e),t.target="#"+e}return _(Rt,t,Qt),t},n._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},n._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},n._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},n._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&(void 0===this._offsets[o+1]||t li > .active":".active";e=(e=V.find(o,i))[e.length-1]}var s=null;if(e&&(s=Q.trigger(e,"hide.bs.tab",{relatedTarget:this._element})),!(Q.trigger(this._element,"show.bs.tab",{relatedTarget:e}).defaultPrevented||null!==s&&s.defaultPrevented)){this._activate(this._element,i);var r=function(){Q.trigger(e,"hidden.bs.tab",{relatedTarget:t._element}),Q.trigger(t._element,"shown.bs.tab",{relatedTarget:e})};n?this._activate(n,n.parentNode,r):r()}}},n._activate=function(t,e,n){var i=this,o=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,".active"):V.find(":scope > li > .active",e))[0],s=n&&o&&o.classList.contains("fade"),r=function(){return i._transitionComplete(t,o,n)};if(o&&s){var a=h(o);o.classList.remove("show"),Q.one(o,"transitionend",r),m(o,a)}else r()},n._transitionComplete=function(t,e,n){if(e){e.classList.remove("active");var i=V.findOne(":scope > .dropdown-menu .active",e.parentNode);i&&i.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}(t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),y(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&t.parentNode.classList.contains("dropdown-menu"))&&(t.closest(".dropdown")&&V.find(".dropdown-toggle").forEach((function(t){return t.classList.add("active")})),t.setAttribute("aria-expanded",!0));n&&n()},e.jQueryInterface=function(t){return this.each((function(){var n=L(this,"bs.tab")||new e(this);if("string"==typeof t){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},o(e,null,[{key:"DATA_KEY",get:function(){return"bs.tab"}}]),e}(U);Q.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){t.preventDefault(),(L(this,"bs.tab")||new Wt(this)).show()})),E((function(){var t=w();if(t){var e=t.fn.tab;t.fn.tab=Wt.jQueryInterface,t.fn.tab.Constructor=Wt,t.fn.tab.noConflict=function(){return t.fn.tab=e,Wt.jQueryInterface}}}));var Ft={animation:"boolean",autohide:"boolean",delay:"number"},Yt={animation:!0,autohide:!0,delay:5e3},zt=function(t){function e(e,n){var i;return(i=t.call(this,e)||this)._config=i._getConfig(n),i._timeout=null,i._setListeners(),i}r(e,t);var n=e.prototype;return n.show=function(){var t=this;if(!Q.trigger(this._element,"show.bs.toast").defaultPrevented){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var e=function(){t._element.classList.remove("showing"),t._element.classList.add("show"),Q.trigger(t._element,"shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove("hide"),y(this._element),this._element.classList.add("showing"),this._config.animation){var n=h(this._element);Q.one(this._element,"transitionend",e),m(this._element,n)}else e()}},n.hide=function(){var t=this;if(this._element.classList.contains("show")&&!Q.trigger(this._element,"hide.bs.toast").defaultPrevented){var e=function(){t._element.classList.add("hide"),Q.trigger(t._element,"hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var n=h(this._element);Q.one(this._element,"transitionend",e),m(this._element,n)}else e()}},n.dispose=function(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),Q.off(this._element,"click.dismiss.bs.toast"),t.prototype.dispose.call(this),this._config=null},n._getConfig=function(t){return t=s({},Yt,q.getDataAttributes(this._element),"object"==typeof t&&t?t:{}),_("toast",t,this.constructor.DefaultType),t},n._setListeners=function(){var t=this;Q.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',(function(){return t.hide()}))},n._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},e.jQueryInterface=function(t){return this.each((function(){var n=L(this,"bs.toast");if(n||(n=new e(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t](this)}}))},o(e,null,[{key:"DefaultType",get:function(){return Ft}},{key:"Default",get:function(){return Yt}},{key:"DATA_KEY",get:function(){return"bs.toast"}}]),e}(U);return E((function(){var t=w();if(t){var e=t.fn.toast;t.fn.toast=zt.jQueryInterface,t.fn.toast.Constructor=zt,t.fn.toast.noConflict=function(){return t.fn.toast=e,zt.jQueryInterface}}})),{Alert:F,Button:Y,Carousel:et,Collapse:st,Dropdown:mt,Modal:bt,Popover:Mt,ScrollSpy:Ut,Tab:Wt,Toast:zt,Tooltip:It}})); 7 | //# sourceMappingURL=bootstrap.min.js.map -------------------------------------------------------------------------------- /user/static/user/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.0.0-beta1 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){if(t&&t.__esModule)return t;var e=Object.create(null);return t&&Object.keys(t).forEach((function(n){if("default"!==n){var i=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,i.get?i:{enumerable:!0,get:function(){return t[n]}})}})),e.default=t,Object.freeze(e)}var n=e(t);function i(t,e){for(var n=0;n0,i._pointerEvent=Boolean(window.PointerEvent),i._addEventListeners(),i}r(e,t);var n=e.prototype;return n.next=function(){this._isSliding||this._slide("next")},n.nextWhenVisible=function(){!document.hidden&&v(this._element)&&this.next()},n.prev=function(){this._isSliding||this._slide("prev")},n.pause=function(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(p(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},n.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},n.to=function(t){var e=this;this._activeElement=V.findOne(".active.carousel-item",this._element);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)Q.one(this._element,"slid.bs.carousel",(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var i=t>n?"next":"prev";this._slide(i,this._items[t])}},n.dispose=function(){t.prototype.dispose.call(this),Q.off(this._element,G),this._items=null,this._config=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},n._getConfig=function(t){return t=s({},Z,t),_($,t,J),t},n._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},n._addEventListeners=function(){var t=this;this._config.keyboard&&Q.on(this._element,"keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&(Q.on(this._element,"mouseenter.bs.carousel",(function(e){return t.pause(e)})),Q.on(this._element,"mouseleave.bs.carousel",(function(e){return t.cycle(e)}))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()},n._addTouchEventListeners=function(){var t=this,e=function(e){t._pointerEvent&&tt[e.pointerType.toUpperCase()]?t.touchStartX=e.clientX:t._pointerEvent||(t.touchStartX=e.touches[0].clientX)},n=function(e){t._pointerEvent&&tt[e.pointerType.toUpperCase()]&&(t.touchDeltaX=e.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};V.find(".carousel-item img",this._element).forEach((function(t){Q.on(t,"dragstart.bs.carousel",(function(t){return t.preventDefault()}))})),this._pointerEvent?(Q.on(this._element,"pointerdown.bs.carousel",(function(t){return e(t)})),Q.on(this._element,"pointerup.bs.carousel",(function(t){return n(t)})),this._element.classList.add("pointer-event")):(Q.on(this._element,"touchstart.bs.carousel",(function(t){return e(t)})),Q.on(this._element,"touchmove.bs.carousel",(function(e){return function(e){e.touches&&e.touches.length>1?t.touchDeltaX=0:t.touchDeltaX=e.touches[0].clientX-t.touchStartX}(e)})),Q.on(this._element,"touchend.bs.carousel",(function(t){return n(t)})))},n._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.key){case"ArrowLeft":t.preventDefault(),this.prev();break;case"ArrowRight":t.preventDefault(),this.next()}},n._getItemIndex=function(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)},n._getItemByDirection=function(t,e){var n="next"===t,i="prev"===t,o=this._getItemIndex(e),s=this._items.length-1;if((i&&0===o||n&&o===s)&&!this._config.wrap)return e;var r=(o+("prev"===t?-1:1))%this._items.length;return-1===r?this._items[this._items.length-1]:this._items[r]},n._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(V.findOne(".active.carousel-item",this._element));return Q.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:i,to:n})},n._setActiveIndicatorElement=function(t){if(this._indicatorsElement){for(var e=V.find(".active",this._indicatorsElement),n=0;n0)for(var i=0;i0&&s--,"ArrowDown"===t.key&&sdocument.documentElement.clientHeight;e||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");var n=h(this._dialog);Q.off(this._element,"transitionend"),Q.one(this._element,"transitionend",(function(){t._element.classList.remove("modal-static"),e||(Q.one(t._element,"transitionend",(function(){t._element.style.overflowY=""})),m(t._element,n))})),m(this._element,n),this._element.focus()}},n._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;(!this._isBodyOverflowing&&t&&!T||this._isBodyOverflowing&&!t&&T)&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),(this._isBodyOverflowing&&!t&&!T||!this._isBodyOverflowing&&t&&T)&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},n._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},n._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",container:!1,fallbackPlacements:null,boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:Tt,popperConfig:null},Ot={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},It=function(e){function i(t,i){var o;if(void 0===n)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");return(o=e.call(this,t)||this)._isEnabled=!0,o._timeout=0,o._hoverState="",o._activeTrigger={},o._popper=null,o.config=o._getConfig(i),o.tip=null,o._setListeners(),o}r(i,e);var a=i.prototype;return a.enable=function(){this._isEnabled=!0},a.disable=function(){this._isEnabled=!1},a.toggleEnabled=function(){this._isEnabled=!this._isEnabled},a.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=L(t.delegateTarget,e);n||(n=new this.constructor(t.delegateTarget,this._getDelegateConfig()),A(t.delegateTarget,e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}},a.dispose=function(){clearTimeout(this._timeout),Q.off(this._element,this.constructor.EVENT_KEY),Q.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.parentNode.removeChild(this.tip),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.config=null,this.tip=null,e.prototype.dispose.call(this)},a.show=function(){var e=this;if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(this.isWithContent()&&this._isEnabled){var n=Q.trigger(this._element,this.constructor.Event.SHOW),i=function t(e){if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){var n=e.getRootNode();return n instanceof ShadowRoot?n:null}return e instanceof ShadowRoot?e:e.parentNode?t(e.parentNode):null}(this._element),o=null===i?this._element.ownerDocument.documentElement.contains(this._element):i.contains(this._element);if(n.defaultPrevented||!o)return;var s=this.getTipElement(),r=c(this.constructor.NAME);s.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&s.classList.add("fade");var a="function"==typeof this.config.placement?this.config.placement.call(this,s,this._element):this.config.placement,l=this._getAttachment(a);this._addAttachmentClass(l);var u=this._getContainer();A(s,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||u.appendChild(s),Q.trigger(this._element,this.constructor.Event.INSERTED),this._popper=t.createPopper(this._element,s,this._getPopperConfig(l)),s.classList.add("show");var d,f,p="function"==typeof this.config.customClass?this.config.customClass():this.config.customClass;if(p)(d=s.classList).add.apply(d,p.split(" "));if("ontouchstart"in document.documentElement)(f=[]).concat.apply(f,document.body.children).forEach((function(t){Q.on(t,"mouseover",(function(){}))}));var g=function(){var t=e._hoverState;e._hoverState=null,Q.trigger(e._element,e.constructor.Event.SHOWN),"out"===t&&e._leave(null,e)};if(this.tip.classList.contains("fade")){var _=h(this.tip);Q.one(this.tip,"transitionend",g),m(this.tip,_)}else g()}},a.hide=function(){var t=this;if(this._popper){var e=this.getTipElement(),n=function(){"show"!==t._hoverState&&e.parentNode&&e.parentNode.removeChild(e),t._cleanTipClass(),t._element.removeAttribute("aria-describedby"),Q.trigger(t._element,t.constructor.Event.HIDDEN),t._popper&&(t._popper.destroy(),t._popper=null)};if(!Q.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented){var i;if(e.classList.remove("show"),"ontouchstart"in document.documentElement)(i=[]).concat.apply(i,document.body.children).forEach((function(t){return Q.off(t,"mouseover",b)}));if(this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this.tip.classList.contains("fade")){var o=h(e);Q.one(e,"transitionend",n),m(e,o)}else n();this._hoverState=""}}},a.update=function(){null!==this._popper&&this._popper.update()},a.isWithContent=function(){return Boolean(this.getTitle())},a.getTipElement=function(){if(this.tip)return this.tip;var t=document.createElement("div");return t.innerHTML=this.config.template,this.tip=t.children[0],this.tip},a.setContent=function(){var t=this.getTipElement();this.setElementContent(V.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")},a.setElementContent=function(t,e){if(null!==t)return"object"==typeof e&&g(e)?(e.jquery&&(e=e[0]),void(this.config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this.config.html?(this.config.sanitize&&(e=kt(e,this.config.allowList,this.config.sanitizeFn)),t.innerHTML=e):t.textContent=e)},a.getTitle=function(){var t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this._element):this.config.title),t},a.updateAttachment=function(t){return"right"===t?"end":"left"===t?"start":t},a._getPopperConfig=function(t){var e=this,n={name:"flip",options:{altBoundary:!0}};return this.config.fallbackPlacements&&(n.options.fallbackPlacements=this.config.fallbackPlacements),s({},{placement:t,modifiers:[n,{name:"preventOverflow",options:{rootBoundary:this.config.boundary}},{name:"arrow",options:{element:"."+this.constructor.NAME+"-arrow"}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:function(t){return e._handlePopperPlacementChange(t)}}],onFirstUpdate:function(t){t.options.placement!==t.placement&&e._handlePopperPlacementChange(t)}},this.config.popperConfig)},a._addAttachmentClass=function(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))},a._getContainer=function(){return!1===this.config.container?document.body:g(this.config.container)?this.config.container:V.findOne(this.config.container)},a._getAttachment=function(t){return St[t.toUpperCase()]},a._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)Q.on(t._element,t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==e){var n="hover"===e?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i="hover"===e?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;Q.on(t._element,n,t.config.selector,(function(e){return t._enter(e)})),Q.on(t._element,i,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t._element&&t.hide()},Q.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=s({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},a._fixTitle=function(){var t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))},a._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||L(t.delegateTarget,n))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),A(t.delegateTarget,n,e)),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){"show"===e._hoverState&&e.show()}),e.config.delay.show):e.show())},a._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||L(t.delegateTarget,n))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),A(t.delegateTarget,n,e)),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){"out"===e._hoverState&&e.hide()}),e.config.delay.hide):e.hide())},a._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},a._getConfig=function(t){var e=q.getDataAttributes(this._element);return Object.keys(e).forEach((function(t){Ct.has(t)&&delete e[t]})),t&&"object"==typeof t.container&&t.container.jquery&&(t.container=t.container[0]),"number"==typeof(t=s({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_(At,t,this.constructor.DefaultType),t.sanitize&&(t.template=kt(t.template,t.allowList,t.sanitizeFn)),t},a._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},a._cleanTipClass=function(){var t=this.getTipElement(),e=t.getAttribute("class").match(Lt);null!==e&&e.length>0&&e.map((function(t){return t.trim()})).forEach((function(e){return t.classList.remove(e)}))},a._handlePopperPlacementChange=function(t){var e=t.state;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))},i.jQueryInterface=function(t){return this.each((function(){var e=L(this,"bs.tooltip"),n="object"==typeof t&&t;if((e||!/dispose|hide/.test(t))&&(e||(e=new i(this,n)),"string"==typeof t)){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},o(i,null,[{key:"Default",get:function(){return Nt}},{key:"NAME",get:function(){return At}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return Ot}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Dt}}]),i}(U);E((function(){var t=w();if(t){var e=t.fn[At];t.fn[At]=It.jQueryInterface,t.fn[At].Constructor=It,t.fn[At].noConflict=function(){return t.fn[At]=e,It.jQueryInterface}}}));var jt="popover",Pt=new RegExp("(^|\\s)bs-popover\\S+","g"),xt=s({},It.Default,{placement:"right",trigger:"click",content:"",template:''}),Ht=s({},It.DefaultType,{content:"(string|element|function)"}),Bt={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},Mt=function(t){function e(){return t.apply(this,arguments)||this}r(e,t);var n=e.prototype;return n.isWithContent=function(){return this.getTitle()||this._getContent()},n.setContent=function(){var t=this.getTipElement();this.setElementContent(V.findOne(".popover-header",t),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(V.findOne(".popover-body",t),e),t.classList.remove("fade","show")},n._addAttachmentClass=function(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))},n._getContent=function(){return this._element.getAttribute("data-bs-content")||this.config.content},n._cleanTipClass=function(){var t=this.getTipElement(),e=t.getAttribute("class").match(Pt);null!==e&&e.length>0&&e.map((function(t){return t.trim()})).forEach((function(e){return t.classList.remove(e)}))},e.jQueryInterface=function(t){return this.each((function(){var n=L(this,"bs.popover"),i="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new e(this,i),A(this,"bs.popover",n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},o(e,null,[{key:"Default",get:function(){return xt}},{key:"NAME",get:function(){return jt}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return Bt}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Ht}}]),e}(It);E((function(){var t=w();if(t){var e=t.fn[jt];t.fn[jt]=Mt.jQueryInterface,t.fn[jt].Constructor=Mt,t.fn[jt].noConflict=function(){return t.fn[jt]=e,Mt.jQueryInterface}}}));var Rt="scrollspy",Kt={offset:10,method:"auto",target:""},Qt={offset:"number",method:"string",target:"(string|element)"},Ut=function(t){function e(e,n){var i;return(i=t.call(this,e)||this)._scrollElement="BODY"===e.tagName?window:e,i._config=i._getConfig(n),i._selector=i._config.target+" .nav-link, "+i._config.target+" .list-group-item, "+i._config.target+" .dropdown-item",i._offsets=[],i._targets=[],i._activeTarget=null,i._scrollHeight=0,Q.on(i._scrollElement,"scroll.bs.scrollspy",(function(t){return i._process(t)})),i.refresh(),i._process(),i}r(e,t);var n=e.prototype;return n.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?"offset":"position",n="auto"===this._config.method?e:this._config.method,i="position"===n?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(this._selector).map((function(t){var e=d(t),o=e?V.findOne(e):null;if(o){var s=o.getBoundingClientRect();if(s.width||s.height)return[q[n](o).top+i,e]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},n.dispose=function(){t.prototype.dispose.call(this),Q.off(this._scrollElement,".bs.scrollspy"),this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},n._getConfig=function(t){if("string"!=typeof(t=s({},Kt,"object"==typeof t&&t?t:{})).target&&g(t.target)){var e=t.target.id;e||(e=c(Rt),t.target.id=e),t.target="#"+e}return _(Rt,t,Qt),t},n._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},n._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},n._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},n._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&(void 0===this._offsets[o+1]||t li > .active":".active";e=(e=V.find(o,i))[e.length-1]}var s=null;if(e&&(s=Q.trigger(e,"hide.bs.tab",{relatedTarget:this._element})),!(Q.trigger(this._element,"show.bs.tab",{relatedTarget:e}).defaultPrevented||null!==s&&s.defaultPrevented)){this._activate(this._element,i);var r=function(){Q.trigger(e,"hidden.bs.tab",{relatedTarget:t._element}),Q.trigger(t._element,"shown.bs.tab",{relatedTarget:e})};n?this._activate(n,n.parentNode,r):r()}}},n._activate=function(t,e,n){var i=this,o=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,".active"):V.find(":scope > li > .active",e))[0],s=n&&o&&o.classList.contains("fade"),r=function(){return i._transitionComplete(t,o,n)};if(o&&s){var a=h(o);o.classList.remove("show"),Q.one(o,"transitionend",r),m(o,a)}else r()},n._transitionComplete=function(t,e,n){if(e){e.classList.remove("active");var i=V.findOne(":scope > .dropdown-menu .active",e.parentNode);i&&i.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}(t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),y(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&t.parentNode.classList.contains("dropdown-menu"))&&(t.closest(".dropdown")&&V.find(".dropdown-toggle").forEach((function(t){return t.classList.add("active")})),t.setAttribute("aria-expanded",!0));n&&n()},e.jQueryInterface=function(t){return this.each((function(){var n=L(this,"bs.tab")||new e(this);if("string"==typeof t){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},o(e,null,[{key:"DATA_KEY",get:function(){return"bs.tab"}}]),e}(U);Q.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){t.preventDefault(),(L(this,"bs.tab")||new Wt(this)).show()})),E((function(){var t=w();if(t){var e=t.fn.tab;t.fn.tab=Wt.jQueryInterface,t.fn.tab.Constructor=Wt,t.fn.tab.noConflict=function(){return t.fn.tab=e,Wt.jQueryInterface}}}));var Ft={animation:"boolean",autohide:"boolean",delay:"number"},Yt={animation:!0,autohide:!0,delay:5e3},zt=function(t){function e(e,n){var i;return(i=t.call(this,e)||this)._config=i._getConfig(n),i._timeout=null,i._setListeners(),i}r(e,t);var n=e.prototype;return n.show=function(){var t=this;if(!Q.trigger(this._element,"show.bs.toast").defaultPrevented){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var e=function(){t._element.classList.remove("showing"),t._element.classList.add("show"),Q.trigger(t._element,"shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove("hide"),y(this._element),this._element.classList.add("showing"),this._config.animation){var n=h(this._element);Q.one(this._element,"transitionend",e),m(this._element,n)}else e()}},n.hide=function(){var t=this;if(this._element.classList.contains("show")&&!Q.trigger(this._element,"hide.bs.toast").defaultPrevented){var e=function(){t._element.classList.add("hide"),Q.trigger(t._element,"hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var n=h(this._element);Q.one(this._element,"transitionend",e),m(this._element,n)}else e()}},n.dispose=function(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),Q.off(this._element,"click.dismiss.bs.toast"),t.prototype.dispose.call(this),this._config=null},n._getConfig=function(t){return t=s({},Yt,q.getDataAttributes(this._element),"object"==typeof t&&t?t:{}),_("toast",t,this.constructor.DefaultType),t},n._setListeners=function(){var t=this;Q.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',(function(){return t.hide()}))},n._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},e.jQueryInterface=function(t){return this.each((function(){var n=L(this,"bs.toast");if(n||(n=new e(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t](this)}}))},o(e,null,[{key:"DefaultType",get:function(){return Ft}},{key:"Default",get:function(){return Yt}},{key:"DATA_KEY",get:function(){return"bs.toast"}}]),e}(U);return E((function(){var t=w();if(t){var e=t.fn.toast;t.fn.toast=zt.jQueryInterface,t.fn.toast.Constructor=zt,t.fn.toast.noConflict=function(){return t.fn.toast=e,zt.jQueryInterface}}})),{Alert:F,Button:Y,Carousel:et,Collapse:st,Dropdown:mt,Modal:bt,Popover:Mt,ScrollSpy:Ut,Tab:Wt,Toast:zt,Tooltip:It}})); 7 | //# sourceMappingURL=bootstrap.min.js.map --------------------------------------------------------------------------------