commit cf40343c5103e0b80a0fe8e863439463bddfd469 Author: yuming Date: Wed Apr 22 21:29:03 2026 +0800 初始化 telegram-downloader 并接入群晖 CI/CD diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5f891f2 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# ============================================= +# Telegram Media Downloader - 环境变量配置模板 +# 复制本文件为 .env,填入真实值后启动 docker compose +# ============================================= + +# 必填:Telegram API 凭证 +# 前往 https://my.telegram.org/apps 申请 +API_ID=你的api_id +API_HASH=你的api_hash + +# 可选:Bot Token(如果用 Bot 模式下载) +# BOT_TOKEN= + +# 可选:下载文件保存路径(容器内路径,默认 /app/downloads) +SAVE_PATH=/app/downloads + +# 可选:Web 界面端口(默认 5000) +WEB_PORT=5000 + +# 可选:界面语言 CN=中文 EN=英文(默认 CN) +LANGUAGE=CN diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..93ae952 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,39 @@ +name: 部署到群晖 + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: 安装 Docker CLI + run: | + apt-get update -qq + apt-get install -y -qq docker.io + + - name: 拉取代码 + uses: actions/checkout@v4 + + - name: 构建镜像 + run: docker build -t telegram-downloader:latest . + + - name: 停止旧容器 + run: | + docker stop telegram-downloader 2>/dev/null || true + docker rm telegram-downloader 2>/dev/null || true + + - name: 启动新容器 + run: | + docker run -d \ + --name telegram-downloader \ + --restart unless-stopped \ + -p 15001:5001 \ + -v /volume1/docker/apps/telegram-downloader/appdata:/app/appdata \ + -v /volume1/docker/apps/telegram-downloader/downloads:/app/downloads \ + telegram-downloader:latest + + - name: 部署完成提示 + run: | + echo "✅ 部署完成,浏览器访问 http://192.168.1.66:15001 查看" diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000..3d8b65d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,85 @@ +name: 🐞 Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug", "triage"] +assignees: + - octocat +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: telegram_media_downloader-version + attributes: + label: telegram_media_downloader version or commit + description: What version of our software are you running? + placeholder: ex. v2.1.5 or 550a063a7fad8723220d8681687d2363cb838d7c + validations: + required: true + - type: dropdown + id: OS + attributes: + label: What OS are you seeing the problem on? + multiple: true + options: + - Mac + - Windows + - Other Linux Distro + validations: + required: false + - type: input + id: python-version + attributes: + label: Python Version + description: What version of python are you running? + placeholder: ex. 3.7.1 + validations: + required: false + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you expected to happen! + validations: + required: true + - type: textarea + id: current-behavior + attributes: + label: Current Behavior + description: Also tell us, what is currently happening? + placeholder: Tell us what is happening now. + validations: + required: true + - type: textarea + id: possible-solution + attributes: + label: Possible Solution + description: Do you have a solution for the issue? + placeholder: Tell us what the solution could look like. + validations: + required: false + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: Tell us how to reproduce the issue? + placeholder: Tell us how to reproduce the issue? + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8a827e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,91 @@ +name: 🐞 Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug", "triage"] +assignees: + - octocat +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: opencommit-version + attributes: + label: Opencommit Version + description: What version of our software are you running? + placeholder: ex. 1.1.22 + validations: + required: true + - type: input + id: node-version + attributes: + label: Node Version + description: What version of node are you running? + placeholder: ex. 19.8.1 + validations: + required: true + - type: input + id: npm-version + attributes: + label: NPM Version + description: What version of npm are you running? + placeholder: ex. 9.6.2 + validations: + required: true + - type: dropdown + id: OS + attributes: + label: What OS are you seeing the problem on? + multiple: true + options: + - Mac + - Windows + - Other Linux Distro + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you expected to happen! + validations: + required: true + - type: textarea + id: current-behavior + attributes: + label: Current Behavior + description: Also tell us, what is currently happening? + placeholder: Tell us what is happening now. + validations: + required: true + - type: textarea + id: possible-solution + attributes: + label: Possible Solution + description: Do you have a solution for the issue? + placeholder: Tell us what the solution could look like. + validations: + required: false + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: Tell us how to reproduce the issue? + placeholder: Tell us how to reproduce the issue? + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/documentation-improvement.md b/.github/ISSUE_TEMPLATE/documentation-improvement.md new file mode 100644 index 0000000..f035739 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-improvement.md @@ -0,0 +1,20 @@ +--- +name: Documentation Improvement +about: Report wrong or missing documentation. +title: 'DOC:' +labels: '' +assignees: '' + +--- + +#### Location of the documentation + +[this should provide the location of the documentation, e.g. "CONTRIBUTION.md" or the URL of the documentation, e.g. "https://github.com/tangyoha/telegram_media_downloader/blob/master/CONTRIBUTING.md"] + +#### Documentation problem + +[this should provide a description of what documentation you believe needs to be fixed/improved] + +#### Suggested fix for documentation + +[this should explain the suggested fix and **why** it's better than the existing documentation] diff --git a/.github/ISSUE_TEMPLATE/featureRequest.yaml b/.github/ISSUE_TEMPLATE/featureRequest.yaml new file mode 100644 index 0000000..b861a6e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/featureRequest.yaml @@ -0,0 +1,48 @@ +--- +name: 🛠️ Feature Request +description: Suggest an idea to help us improve Opencommit +title: "[Feature]: " +labels: + - "feature_request" + +body: + - type: markdown + attributes: + value: | + **Thanks :heart: for taking the time to fill out this feature request report!** + We kindly ask that you search to see if an issue [already exists](https://github.com/di-sukharev/opencommit/issues?q=is%3Aissue+sort%3Acreated-desc+) for your feature. + + We are also happy to accept contributions from our users. For more details see [here](https://github.com/di-sukharev/opencommit/blob/master/.github/CONTRIBUTING.md). + + - type: textarea + attributes: + label: Description + description: | + A clear and concise description of the feature you're interested in. + validations: + required: true + + - type: textarea + attributes: + label: Suggested Solution + description: | + Describe the solution you'd like. A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives + description: | + Describe alternatives you've considered. + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + attributes: + label: Additional Context + description: | + Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..b861a6e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,48 @@ +--- +name: 🛠️ Feature Request +description: Suggest an idea to help us improve Opencommit +title: "[Feature]: " +labels: + - "feature_request" + +body: + - type: markdown + attributes: + value: | + **Thanks :heart: for taking the time to fill out this feature request report!** + We kindly ask that you search to see if an issue [already exists](https://github.com/di-sukharev/opencommit/issues?q=is%3Aissue+sort%3Acreated-desc+) for your feature. + + We are also happy to accept contributions from our users. For more details see [here](https://github.com/di-sukharev/opencommit/blob/master/.github/CONTRIBUTING.md). + + - type: textarea + attributes: + label: Description + description: | + A clear and concise description of the feature you're interested in. + validations: + required: true + + - type: textarea + attributes: + label: Suggested Solution + description: | + Describe the solution you'd like. A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives + description: | + Describe alternatives you've considered. + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + attributes: + label: Additional Context + description: | + Add any other context about the problem here. + validations: + required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1c222d7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + time: "12:00" + timezone: CET + open-pull-requests-limit: 10 + reviewers: + - tangyoha diff --git a/.github/issue_label_bot.yaml b/.github/issue_label_bot.yaml new file mode 100644 index 0000000..f842961 --- /dev/null +++ b/.github/issue_label_bot.yaml @@ -0,0 +1,4 @@ +label-alias: + bug: 'kind/bug' + feature_request: 'enhancement' + question: 'question' diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..acdc50a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,22 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 90 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - enhancement + - feature_request + - help wanted + - good first issue + - dependencies + - bug +# Label to use when marking as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity in the past 45 days. It will be closed if no further activity + occurs in the next 7 days. Thank you for your contributions. + +# Limit to only `issues` +only: issues diff --git a/.github/workflows/code-checks.yml b/.github/workflows/code-checks.yml new file mode 100644 index 0000000..a944e44 --- /dev/null +++ b/.github/workflows/code-checks.yml @@ -0,0 +1,24 @@ +name: Code Quality + +on: + pull_request: + branches: [ master ] + paths-ignore: + - 'README.md' + push: + branches: [ master ] + paths-ignore: + - 'README.md' + +jobs: + pre-commit: + name: Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10.8' + - name: Install dependencies + run: make dev_install + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/docker-publish-release.yaml b/.github/workflows/docker-publish-release.yaml new file mode 100644 index 0000000..daf9b08 --- /dev/null +++ b/.github/workflows/docker-publish-release.yaml @@ -0,0 +1,123 @@ +name: Create Release and Upload Assets + +on: + push: + tags: + - 'v*' + +jobs: + build-tdl-binaries: + name: Build tdl binaries for ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] #[macos-latest, ubuntu-20.04, windows-latest] + include: + - os: macos-latest + TARGET: macos + - os: ubuntu-20.04 + TARGET: linux-amd64 + - os: windows-latest + TARGET: win64 + container: ${{ matrix.CONTAINER }} + env: + DISTPATH: tdl-${{ matrix.TARGET }} + steps: + - name: Checkout repository + uses: actions/checkout@master + - name: Set up Python 3.11 + uses: actions/setup-python@master + with: + python-version: 3.11 + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + python3 -m pip install -r requirements.txt + python3 gen_filter_cache.py + pip install pyinstaller==6.7.0 + - name: Build with PyInstaller + run: | + pyinstaller --distpath ./${{ env.DISTPATH }} media_downloader.spec + - name: Add license and readme + shell: bash + run: mv README_CN.md README.md ./${{ env.DISTPATH }} + - name: Archive artifact + uses: actions/upload-artifact@master + with: + name: ${{ env.DISTPATH }} + path: ${{ env.DISTPATH }} + + # release: + # needs: build-tdl-binaries + # runs-on: ubuntu-latest + # steps: + # - name: Check out code + # uses: actions/checkout@v2 + # with: + # fetch-depth: 0 + + # - name: Get Release Date + # id: release_date + # run: | + # RELEASE_DATE=$(date +%Y-%m-%d) + # echo "RELEASE_DATE=$RELEASE_DATE" | tee -a $GITHUB_ENV + # echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT + + # - name: Generate changelog + # id: changelog + # run: | + # PREVIOUS_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`) + # CURRENT_TAG=${{ github.ref }} + # LOG=$(git log --pretty=format:'* %s by @%an in %H' $PREVIOUS_TAG...$CURRENT_TAG) + # echo "changelog=$LOG" >> $GITHUB_OUTPUT + + create_release: + name: Create GitHub release + if: startsWith(github.ref, 'refs/tags/') + needs: build-tdl-binaries + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + VERSION: ${{ steps.get_version.outputs.VERSION }} + steps: + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }},${{ steps.release_date.outputs.release_date }} + draft: false + prerelease: false + - name: Get version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + shell: bash + + upload_assets: + name: Upload release assets + if: startsWith(github.ref, 'refs/tags/') + needs: create_release + runs-on: ubuntu-latest + strategy: + matrix: + TARGET: [win64] #[macos, linux-amd64, win64] + env: + DISTPATH: tdl-${{ needs.create_release.outputs.VERSION }}-${{ matrix.TARGET }} + steps: + - name: Download built binaries + uses: actions/download-artifact@master + - name: Rename and package binaries + run: | + zip -r ${{ env.DISTPATH }}.zip ./tdl-${{ matrix.TARGET }}/* + - name: Upload release assets + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: "${{ env.DISTPATH }}.zip" + asset_name: "${{ env.DISTPATH }}.zip" + asset_content_type: application/zip diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..6ee24e0 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,80 @@ +name: Docker Publish + +on: + push: + branches: + - master + tags: + - 'v*' + +jobs: + build-and-push: + runs-on: ubuntu-latest + outputs: + compile_image_exists: ${{ steps.check-image.outputs.exists }} + requirements_modified: ${{ steps.check-requirements.outputs.modified }} + check-dockerfile: ${{ steps.check-dockerfile.outputs.dockerfile_modified }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Check if compile-image exists on Docker Hub + id: check-image + run: | + EXISTS=$(curl --silent --fail --head "https://hub.docker.com/v2/repositories/${{ secrets.DOCKER_HUB_USERNAME }}/telegram_media_downloader_compile/tags/latest" > /dev/null && echo "true" || echo "false") + echo "exists=$EXISTS" >> $GITHUB_ENV + echo "exists=$EXISTS" >> $GITHUB_OUTPUT + + - name: Check if requirements.txt has been modified + id: check-requirements + run: | + MODIFIED=$(git diff --name-only HEAD~1 HEAD | grep -w 'requirements.txt' > /dev/null && echo "true" || echo "false") + echo "modified=$MODIFIED" >> $GITHUB_ENV + echo "modified=$MODIFIED" >> $GITHUB_OUTPUT + + - name: Check if Dockerfile has been modified + id: check-dockerfile + run: | + DOCKERFILE_MODIFIED=$(git diff --name-only HEAD~1 HEAD | grep -w 'Dockerfile' > /dev/null && echo "true" || echo "false") + echo "dockerfile_modified=$DOCKERFILE_MODIFIED" >> $GITHUB_ENV + echo "dockerfile_modified=$DOCKERFILE_MODIFIED" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Print Env + id: env_aa + run: | + echo ${{ steps.check-image.outputs.exists }} + echo ${{ steps.check-requirements.outputs.modified }} + echo ${{ steps.check-dockerfile.outputs.dockerfile_modified }} + + - name: Build and push compile-image + if: ${{ !(steps.check-image.outputs.exists == 'true' && steps.check-requirements.outputs.modified == 'false' && steps.check-dockerfile.outputs.dockerfile_modified == 'false') }} + uses: docker/build-push-action@v4 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/386,linux/ppc64le + target: compile-image + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/telegram_media_downloader_compile:latest + + - name: Build and push runtime-image + uses: docker/build-push-action@v4 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/386,linux/ppc64le + target: runtime-image + tags: | + ${{ secrets.DOCKER_HUB_USERNAME }}/telegram_media_downloader:latest + ${{ secrets.DOCKER_HUB_USERNAME }}/telegram_media_downloader:${{ github.ref_name }} diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..3b51279 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,42 @@ +name: Unittest + +on: + push: + branches: [ master ] + paths-ignore: + - 'README.md' + pull_request: + branches: [ master ] + paths-ignore: + - 'README.md' + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12' ] + name: Test - Python ${{ matrix.python-version }} on ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Get setuptools Unix + if: ${{ matrix.os != 'windows-latest' }} + run: python -m pip install --upgrade pip setuptools codecov + - name: Get setuptools Windows + if: ${{ matrix.os == 'windows-latest' }} + run: python -m pip install --upgrade pip setuptools codecov + - name: Install dependencies + run: make dev_install + - name: Test with pytest + run: | + make -e test + codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f0fc0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +.idea/ +*.pyc +*.pid +*.cfg +*.db +*.env +.DS_Store +.cache/ +.mypy_cache/ +.coverage +settings.json + +# Distribution / packaging +.Python +.pytest_cache +.python-version +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +logs/ +parts/ +sdist/ +share/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + + +# Jupyter Notebook +.ipynb_checkpoints +*.ipynb + +# virtualenv +.venv +venv/ +ENV/ +bin/ +include/ +pip-selfcheck.json +lib64 + +#Telegram Sessions +*.session +*.session-journal + +#Downloaded documents +documents/ +audio/ +document/ +photo/ +voice/ +video/ +video_note/ +parser.out +parsetab.py +local_test/ +.vscode +TODO.md +log/ +temp/ +config.yaml +data.yaml + +# 持久化数据目录(宿主机管理,不进 git) +appdata/ +sessions/ +channel_history.json + +# 本地打包产物 +tmd.tar.gz +/d +/d/ + +# 个人部署笔记,不公开 +DEPLOY_TO_SYNOLOGY.md +*.scss +*.css.map diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..de3c904 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + name: black + entry: black + types: [python] +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort + entry: isort + types: [python] + args: ["--profile", "black", "--filter-files"] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.961 + hooks: + - id: mypy + name: mypy + entry: mypy + types: [python] + args: [--ignore-missing-imports] + files: utils/|media_downloader.py|module/ + exclude: tests/|module/static/|module/templates +- repo: https://github.com/pycqa/pylint + rev: v2.14.5 + hooks: + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: [ + "-rn", # Only display messages + "-sn", # Don't display the score + "--rcfile=pylintrc" # Link to your config file + ] + files: utils/|media_downloader.py|module/ + exclude: tests/|module/static/|module/templates diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..dc429ea --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at tangyoha@outlook.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4cc2c7e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,199 @@ +## Contributing + +First off, thank you for considering contributing to Telegram Media Downloader. It's people like you that make telegram-media-downloader such a great tool. +Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. + +### Where do I go from here? + +If you've noticed a bug or have a feature request, [make one](https://github.com/tangyoha/telegram_media_downloader/issues)! It's generally best if you get confirmation of your bug or approval for your feature request this way before starting to code. + +If you have a general question about telegram-media-downloader, you can ask it on [Discussion](https://github.com/tangyoha/telegram_media_downloader/discussions) under `Q&A` category and any ideas/suggestions goes under `Ideas` category, the issue tracker is only for bugs and feature requests. + +### Fork & create a branch + +If this is something you think you can fix, then [fork telegram-media-downloader](https://help.github.com/articles/fork-a-repo) and create a branch with a descriptive name. + +A good branch name would be (where issue #52 is the ticket you're working on): + +```sh + git checkout -b 52-fix-expired-file-reference +``` + +### For new Contributors + +If you never created a pull request before, welcome [Here is a great tutorial](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) on how to send one :) + +1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, and configure the remotes: +```sh + # Clone your fork of the repo into the current directory + git clone https://github.com// + # Navigate to the newly cloned directory + cd + # Install dependencies + make dev_install + # Assign the original repo to a remote called "upstream" + git remote add upstream https://github.com/Dineshkkarthik/ +``` + +2. If you cloned a while ago, get the latest changes from upstream: +```sh + git checkout master + git pull upstream master +``` + +3. Create a new branch (off the main project master branch) to contain your feature, change, or fix based on the branch name convention described above: +```sh + git checkout -b +``` + +4. Make sure to update, or add to the tests when appropriate. Patches and features will not be accepted without tests. Run `make test` to check that all tests pass after you've made changes. + +5. If you added or changed a feature, make sure to document it accordingly in the `README.md` file. + +6. Push your branch up to your fork: +```sh + git push origin +``` + +7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description. + + +### Coding Standards + +#### Python style + +Please follow these coding standards when writing code for inclusion in telegram-media-downloader. + +Telegram-media-downloader follows the [PEP8](https://www.python.org/dev/peps/pep-0008/) standard and uses [Black](https://black.readthedocs.io/en/stable/) and [Pylint](https://pylint.pycqa.org/en/latest/) to ensure a consistent code format throughout the project. + +[Continuous Integration](https://github.com/tangyoha/telegram_media_downloader/actions) using GitHub Actions will run those tools and report any stylistic errors in your code. Therefore, it is helpful before submitting code to run the check yourself: +```sh +black media_downloader.py utils +``` +to auto-format your code. Additionally, many editors have plugins that will apply `black` as you edit files. + +Writing good code is not just about what you write. It is also about _how_ you write it. During [Continuous Integration](https://github.com/tangyoha/telegram_media_downloader/actions) testing, several tools will be run to check your code for stylistic errors. Generating any warnings will cause the test to fail. Thus, good style is a requirement for submitting code to telegram-media-downloader. + +This is already added in the repo to help contributors verify their changes before contributing them to the project: +```sh +make style_check +``` + +#### Type hints + +Telegram-media-downloader strongly encourages the use of [**PEP 484**](https://www.python.org/dev/peps/pep-0484) style type hints. New development should contain type hints and pull requests to annotate existing code are accepted as well! + +Types imports should follow the `from typing import ...` convention. So rather than +```py +import typing + +primes: typing.List[int] = [] +``` +You should write +```py +from typing import List, Optional, Union + +primes: List[int] = [] +``` + +`Optional` should be used where applicable, so instead of +```py +maybe_primes: List[Union[int, None]] = [] +``` +You should write +```py +maybe_primes: List[Optional[int]] = [] +``` + +#### Validating type hints + +telegram-media-downloader uses [mypy](http://mypy-lang.org/) to statically analyze the code base and type hints. After making any change you can ensure your type hints are correct by running +```sh +make static_type_check +``` + +#### Docstrings and standards + +A Python docstring is a string used to document a Python module, class, function or method, so programmers can understand what it does without having to read the details of the implementation. + +The next example gives an idea of what a docstring looks like: +```py +def add(num1: int, num2: int) -> int: + """ + Add up two integer numbers. + + This function simply wraps the ``+`` operator, and does not + do anything interesting, except for illustrating what + the docstring of a very simple function looks like. + + Parameters + ---------- + num1: int + First number to add. + num2: int + Second number to add. + + Returns + ------- + int + The sum of ``num1`` and ``num2``. + + See Also + -------- + subtract : Subtract one integer from another. + + Examples + -------- + >>> add(2, 2) + 4 + >>> add(25, 0) + 25 + >>> add(10, -10) + 0 + """ + return num1 + num2 +``` +Some standards regarding docstrings exist, which make them easier to read, and allow them be easily exported to other formats such as html or pdf. + +### Commit Message + +telegram-media-downloader uses a convention for commit message prefixes and layout. Here are some common prefixes along with general guidelines for when to use them: +``` +: +<-- OPTIONAL --> + + +``` + +#### Prefix: + +Must be one of the following: +- **add**: Adding a new file +- **ci**: Changes to CI configuration files and scripts (example: files inside `.github` folder) +- **clean**: Code cleanup +- **docs**: Additions/updates to documentation +- **enh**: Enhancement, new functionality +- **fix**: Bug fix +- **perf**: A code change that improves performance +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) +- **test**: Additions/updates to tests +- **type**: Type annotations + +#### Subject: + +Please reference the relevant GitHub issues in your commit message using #1234. +- a subject line with `< 80` chars. +- summary in present tense. +- not capitalized. +- no period at the end. + +#### Commit Message Body + +Just as in the summary, use the imperative, present tense. + +Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change. You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change. + +### Code of Conduct + +As a contributor, you can help us keep the community open and inclusive. Please read and follow our [Code of Conduct](https://github.com/tangyoha/telegram_media_downloader/blob/master/CODE_OF_CONDUCT.md). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..acfdc1b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.11.9-alpine AS compile-image + +WORKDIR /app + +COPY requirements.txt /app/ + +RUN apk add --no-cache --virtual .build-deps gcc musl-dev libffi-dev openssl-dev \ + && pip install --no-cache-dir -r requirements.txt \ + && apk del .build-deps + +# ───────────────────────────────────────────── +FROM python:3.11.9-alpine AS runtime-image + +WORKDIR /app + +# 从上面自己编译的阶段复制依赖(支持任意架构) +COPY --from=compile-image /usr/local/lib/python3.11/site-packages \ + /usr/local/lib/python3.11/site-packages + +# 复制应用代码(不打包 config.yaml,运行时由 entrypoint 生成) +COPY setup.py media_downloader.py do_login.py /app/ +COPY module /app/module +COPY utils /app/utils +COPY entrypoint.sh /app/entrypoint.sh + +RUN chmod +x /app/entrypoint.sh \ + && mkdir -p /app/downloads /app/appdata + +EXPOSE 5000 + +ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4bef804 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Dineshkarthik R + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MY_SETUP_GUIDE.md b/MY_SETUP_GUIDE.md new file mode 100644 index 0000000..c6f78cf --- /dev/null +++ b/MY_SETUP_GUIDE.md @@ -0,0 +1,339 @@ +# Telegram Media Downloader 配置指南 + +> 本文档记录了在 macOS 上配置和运行 Telegram Media Downloader 的完整步骤。 + +--- + +## 目录 + +- [环境要求](#环境要求) +- [快速启动](#快速启动) +- [详细配置步骤](#详细配置步骤) + - [1. 安装依赖](#1-安装依赖) + - [2. 获取 Telegram API 密钥](#2-获取-telegram-api-密钥) + - [3. 配置代理](#3-配置代理) + - [4. 修改 Web 端口](#4-修改-web-端口) + - [5. 首次登录认证](#5-首次登录认证) + - [6. 配置下载任务](#6-配置下载任务) + - [7. 配置 Bot(持续运行)](#7-配置-bot持续运行) +- [配置文件说明](#配置文件说明) +- [常用命令](#常用命令) +- [常见问题](#常见问题) +- [我的配置信息](#我的配置信息) + +--- + +## 环境要求 + +- **操作系统**: macOS / Linux / Windows +- **Python**: 3.7 及以上 +- **代理软件**: Clash / V2Ray 等(国内必需) +- **Telegram 账号**: 需要能正常登录 + +--- + +## 快速启动 + +如果已经配置好,直接运行: + +```bash +cd /Users/gaotu/WebstormProjects/telegram_media_downloader-master +python3 media_downloader.py +``` + +访问 Web 界面: +- **下载进度**: http://localhost:5001 +- **控制台**: http://localhost:5001/control + +--- + +## 详细配置步骤 + +### 1. 安装依赖 + +```bash +# 进入项目目录 +cd /Users/gaotu/WebstormProjects/telegram_media_downloader-master + +# 安装依赖(macOS/Linux) +make install + +# 或者直接用 pip(Windows) +pip3 install -r requirements.txt +``` + +### 2. 获取 Telegram API 密钥 + +1. 访问 https://my.telegram.org/apps +2. 使用 Telegram 账号登录,登录时需要注意,验证码不是发短信,是发送到软件里,需要到软件中的服务号查收。 +3. 填写表单创建新应用(Platform 选 Desktop) +4. 获取 `api_id` 和 `api_hash` + +> ⚠️ 注意:如果提示 "too many tries",需要等待 1-2 小时再试。 + +### 3. 配置代理 + +由于国内无法直连 Telegram,需要配置代理。 + +编辑 `config.yaml`,添加代理配置: + +```yaml +# Clash 代理配置 +proxy: + scheme: socks5 + hostname: 127.0.0.1 + port: 7891 # Clash SOCKS5 端口,根据实际情况修改 +``` + +**常见代理端口**: + +| 代理软件 | HTTP 端口 | SOCKS5 端口 | +|---------|----------|------------| +| Clash | 7890 | 7891 | +| V2Ray | 10809 | 10808 | +| Shadowsocks | - | 1080 | + +### 4. 修改 Web 端口 + +macOS 的 AirPlay Receiver 默认占用 5000 端口,需要修改: + +```yaml +web_host: 127.0.0.1 +web_port: 5001 # 改为其他端口 +``` + +### 5. 首次登录认证 + +运行程序: + +```bash +python3 media_downloader.py +``` + +首次运行会要求输入: + +1. **手机号**:输入 Telegram 注册手机号(带国际区号,如 `+8615064240820`) +2. **确认**:输入 `y` 确认 +3. **验证码**:查看 Telegram 客户端收到的验证码,输入 + +登录成功后会生成 `media_downloader.session` 文件,后续无需重复登录。 + +### 6. 配置下载任务 + +编辑 `config.yaml` 中的 `chat` 部分: + +```yaml +chat: +- chat_id: happycat03 # 频道用户名或 ID + last_read_message_id: 0 # 从头开始下载设为 0 + download_filter: message_date >= 2024-01-01 00:00:00 # 时间过滤 +``` + +**获取频道 ID 的方法**: + +1. **使用用户名**:直接填写 `@` 后面的部分,如 `happycat03` +2. **使用机器人**:发送频道链接给 [@username_to_id_bot](https://t.me/username_to_id_bot) +3. **Web Telegram**:从 URL 中获取 + +**时间过滤器示例**: + +```yaml +# 下载 2024 年以后的 +download_filter: message_date >= 2024-01-01 00:00:00 + +# 下载指定时间段 +download_filter: message_date >= 2024-01-01 00:00:00 and message_date <= 2024-06-30 23:59:59 + +# 不设置过滤器(下载全部) +# 删除 download_filter 行即可 +``` + +### 7. 配置 Bot(持续运行) + +默认情况下,程序下载完成后会自动退出。配置 Bot 后程序会持续运行。 + +**创建 Bot**: + +1. 在 Telegram 中打开 [@BotFather](https://t.me/BotFather) +2. 发送 `/newbot` +3. 按提示设置名称和用户名(用户名必须以 `bot` 结尾) +4. 获取 Bot Token + +**添加到配置**: + +```yaml +bot_token: 你的Bot_Token +``` + +配置后: +- 程序会持续运行 +- 可以通过机器人发送命令下载 +- Web 界面可以一直访问 + +--- + +## 配置文件说明 + +完整的 `config.yaml` 示例: + +```yaml +# Telegram API 密钥 +api_hash: your_api_hash +api_id: your_api_id + +# Bot Token(可选,配置后程序持续运行) +bot_token: your_bot_token + +# 下载任务配置 +chat: +- chat_id: channel_username # 频道用户名或 ID + last_read_message_id: 0 # 上次读取位置,0 表示从头开始 + download_filter: message_date >= 2024-01-01 00:00:00 # 时间过滤 + +# 下载的媒体类型 +media_types: +- audio +- photo +- video +- document +- voice +- video_note + +# 文件格式过滤 +file_formats: + audio: + - all + document: + - all + video: + - all + +# 保存路径 +save_path: /Users/gaotu/Downloads/telegram_downloads + +# 文件路径前缀(子目录结构) +file_path_prefix: +- chat_title # 频道名称 +- media_datetime # 媒体日期 + +# Web 界面配置 +web_host: 127.0.0.1 +web_port: 5001 + +# 代理配置 +proxy: + scheme: socks5 + hostname: 127.0.0.1 + port: 7891 + +# 语言 +language: ZH # ZH 中文, EN 英文 +``` + +--- + +## 常用命令 + +```bash +# 启动下载程序 +python3 media_downloader.py + +# 停止程序 +Ctrl + C + +# 后台运行(可选) +nohup python3 media_downloader.py > download.log 2>&1 & + +# 查看后台进程 +ps aux | grep media_downloader + +# 停止后台进程 +pkill -f media_downloader +``` + +--- + +## 常见问题 + +### Q1: 连接超时(Connection timed out) + +**原因**:代理未配置或代理软件未运行 + +**解决**: +1. 确保 Clash/V2Ray 正在运行 +2. 检查 `config.yaml` 中的代理端口是否正确 + +### Q2: 端口 5000 被占用 + +**原因**:macOS AirPlay Receiver 占用 + +**解决**:修改 `web_port` 为其他端口(如 5001) + +### Q3: 程序下载完就退出 + +**原因**:未配置 Bot Token + +**解决**:创建 Telegram Bot 并配置 `bot_token` + +### Q4: 大文件下载失败(Request timed out) + +**原因**:网络不稳定,大文件容易超时 + +**解决**: +1. 重新运行程序,会自动重试失败的文件 +2. 更换更稳定的代理节点 + +### Q5: API 密钥申请被限制 + +**原因**:尝试次数过多 + +**解决**:等待 1-2 小时后重试,或更换网络/IP + +--- + +## 我的配置信息 + +> ⚠️ 以下是我的个人配置,请勿泄露给他人 + +| 配置项 | 值 | +|--------|-----| +| api_id | `35804872` | +| api_hash | `79872da654ba47a407ff758df43b342b` | +| bot_token | `8304994550:AAHrfD8iZy-g7eMvd5j_nUCWAVzkfpn-STk` | +| Bot 用户名 | [@ymxixibot](https://t.me/ymxixibot) | +| 代理端口 | `7891`(Clash SOCKS5) | +| Web 端口 | `5001` | +| 保存路径 | `/Users/gaotu/Downloads/telegram_downloads` | + +--- + +## Web 界面说明 + +### 下载进度页面 + +地址:http://localhost:5001 + +- 查看正在下载的文件 +- 查看已完成的下载 +- 暂停/继续下载 + +### 控制台页面(自定义) + +地址:http://localhost:5001/control + +- 输入频道链接 +- 设置下载时间范围 +- 查看当前配置 +- 查看下载状态 + +--- + +## 更新日志 + +- **2026-01-10**: 初次配置,成功下载 184 个无损音乐文件 +- **2026-01-10**: 配置 Bot Token,实现程序持续运行 +- **2026-01-10**: 添加自定义 Web 控制台页面 + +--- + +*文档创建于 2026-01-10* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..44d3589 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +TEST_ARTIFACTS ?= /tmp/coverage + +.PHONY: install dev_install static_type_check pylint style_check test + +install: + python3 -m pip install --upgrade pip setuptools + python3 -m pip install -r requirements.txt + +dev_install: install + python3 -m pip install -r dev-requirements.txt + +static_type_check: + mypy media_downloader.py utils module --ignore-missing-imports + +pylint: + pylint media_downloader.py utils module -r y + +style_check: static_type_check pylint + +test: + py.test --cov media_downloader --doctest-modules \ + --cov utils \ + --cov-report term-missing \ + --cov-report html:${TEST_ARTIFACTS} \ + --junit-xml=${TEST_ARTIFACTS}/media-downloader.xml \ + tests/ diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..397d2fc --- /dev/null +++ b/PRD.md @@ -0,0 +1,1298 @@ +# Telegram Media Downloader — 产品需求文档(PRD) + +**文档版本:** 1.0 +**项目版本:** 2.2.5 +**撰写日期:** 2026-04-06 +**项目地址:** https://github.com/tangyoha/telegram_media_downloader + +--- + +## 目录 + +1. [产品概述](#1-产品概述) +2. [目标用户](#2-目标用户) +3. [使用场景](#3-使用场景) +4. [核心功能](#4-核心功能) +5. [项目架构](#5-项目架构) +6. [目录结构](#6-目录结构) +7. [核心模块详解](#7-核心模块详解) +8. [配置项完整说明](#8-配置项完整说明) +9. [下载过滤器语法](#9-下载过滤器语法) +10. [Web 界面 API 接口](#10-web-界面-api-接口) +11. [数据结构](#11-数据结构) +12. [下载目录结构](#12-下载目录结构) +13. [安装与运行](#13-安装与运行) +14. [依赖说明](#14-依赖说明) +15. [当前限制与已知问题](#15-当前限制与已知问题) +16. [优化建议与迭代方向](#16-优化建议与迭代方向) + +--- + +## 1. 产品概述 + +### 1.1 产品定位 + +Telegram Media Downloader 是一款基于 Python 的 Telegram 媒体批量下载工具。它通过 Telegram 官方 MTProto API(pyrogram 客户端)连接 Telegram,能够从指定频道、群组或私聊中批量下载多种媒体类型(视频、图片、音频、文档、语音等),并提供配套的 Web 可视化管理界面和 Telegram Bot 控制接口。 + +### 1.2 核心价值 + +- **批量高效**:支持对多个频道并发处理,断点续传,不重复下载已有文件。 +- **精细过滤**:通过类 SQL 表达式对消息日期、文件大小、文件名、视频分辨率、标题内容等进行精确筛选。 +- **多端控制**:提供 Web 界面(浏览器访问)和 Telegram Bot 两种控制方式,无需每次修改配置文件。 +- **可扩展存储**:下载完成后可自动上传到 Telegram 指定聊天,或通过 Rclone 上传到 100+ 种云存储(Google Drive、OneDrive、阿里云盘等)。 +- **容器化支持**:完整的 Docker/Docker-Compose 支持,易于部署和隔离运行。 + +### 1.3 版本说明 + +当前版本为 **2.2.5**(见 `utils/__init__.py`)。该版本已经历多次重构,核心下载引擎稳定,Web 界面为近期新增功能,仍处于持续迭代阶段。 + +--- + +## 2. 目标用户 + +| 用户类型 | 使用场景 | 技术门槛 | +|---------|---------|---------| +| 内容收藏者 | 批量下载 Telegram 频道的学习资料、影视资源、图片合集 | 低(Web 界面操作) | +| 内容创作者 | 从特定群组归档历史素材 | 低 | +| 开发者/研究者 | 数据采集、内容存档 | 中(需配置 API) | +| 运营人员 | 多频道内容监控、素材备份 | 中 | +| 服务器管理员 | 无头服务器 + Docker 自动化归档 | 高 | + +**前提要求:** + +1. 拥有 Telegram 账号 +2. 在 [my.telegram.org/apps](https://my.telegram.org/apps) 申请个人 API 凭证(免费,约 1 分钟) +3. 目标频道/群组可访问(公开频道或已加入的私有群组) +4. 中国大陆用户需配置代理(Telegram 在大陆被屏蔽) + +--- + +## 3. 使用场景 + +### 场景 A:一次性归档某个频道的历史内容 + +用户希望把某个影视资源 Telegram 频道自 2023 年以来发布的所有 MP4 视频下载到本地。 + +**操作流程:** +1. 打开 Web 界面,在顶部表单输入频道名(如 `@example_channel`) +2. 设置开始日期 `2023-01-01`,验证频道有效性 +3. 在 `config.yaml` 中配置 `file_formats.video: [mp4]`(或在界面中设置) +4. 点击「保存并重启下载」,程序自动遍历频道历史消息,过滤并下载所有 MP4 文件 +5. 下载完成后,文件按 `频道名/年月/` 目录结构整理好 + +--- + +### 场景 B:持续监听新频道更新 + +用户希望每次运行都只下载上次未下载的新内容(增量同步)。 + +**机制:** 每次下载完成后,程序自动把当前最大消息 ID 写入 `config.yaml` 的 `last_read_message_id` 字段,下次运行从该 ID 之后继续,不重复下载。 + +--- + +### 场景 C:通过 Telegram Bot 远程控制 + +用户在服务器上部署程序,配置 `bot_token`,通过向 Bot 发送命令来添加下载任务、查看进度,无需 SSH 进服务器。 + +--- + +### 场景 D:高清视频筛选下载 + +频道中混合了低清和高清视频,用户只想要 1080p 及以上的内容。 + +**配置过滤器:** +```yaml +download_filter: media_width >= 1920 and media_height >= 1080 +``` + +--- + +### 场景 E:大文件上传至云盘 + +本地磁盘空间有限,下载后自动通过 Rclone 上传到 Google Drive,并删除本地文件。 + +--- + +## 4. 核心功能 + +### 4.1 媒体批量下载 + +- **支持媒体类型:** audio(音频)、photo(图片)、video(视频)、document(文档/PDF/压缩包等)、voice(语音消息)、video_note(圆形视频消息)、animation(GIF) +- **文件格式过滤:** 对每种媒体类型可配置允许的格式列表,`all` 表示不限制 +- **并发下载:** 通过 asyncio 队列管理,支持配置最大并发任务数(`max_download_task`)和最大并发传输数(`max_concurrent_transmissions`) +- **断点续传:** 通过 `last_read_message_id` 记录上次处理到的消息位置,下次启动自动从该位置继续 +- **去重跳过:** 已存在于目标路径且大小一致的文件自动跳过,不重复下载 +- **临时目录机制:** 下载先写入 `temp/` 目录,完成后移动到最终位置,避免半完成文件污染 + +### 4.2 下载过滤器 + +基于 PLY(Python Lex-Yacc)实现的表达式过滤引擎,支持: +- 按消息日期筛选(最常用) +- 按文件大小筛选 +- 按视频分辨率筛选 +- 按文件名正则匹配 +- 按消息标题关键字筛选 +- 按视频时长筛选 +- 逻辑组合:`and`、`or` + +详见 [第9节:下载过滤器语法](#9-下载过滤器语法)。 + +### 4.3 Web 管理界面 + +- **顶部操作栏(横排):** 频道输入+验证、开始/结束日期、保存路径编辑、保存并重启按钮 +- **实时统计头部:** 下载速度、正在下载数、已完成数、已跳过数 +- **当前任务横幅:** 显示正在处理的频道名及当前状态 +- **历史频道网格:** 记录历史使用的频道,点击可快速填入表单 +- **下载进度列表:** 实时展示每个文件的下载进度条、速度 +- **已完成列表:** 展示已完成下载的文件名和大小 +- **暂停/继续控制:** 随时暂停或恢复下载任务 +- **新用户引导向导:** 首次使用时显示 3 步引导(API 凭证获取、代理配置、确认),无需手动编辑配置文件 + +### 4.4 Telegram Bot 控制 + +通过配置 `bot_token` 启用机器人模式: +- 向 Bot 发送频道链接,Bot 自动添加下载任务 +- 发送命令查看当前下载进度 +- 转发消息给 Bot,Bot 自动下载其中的媒体 +- 权限控制(可限制只有特定用户能使用) + +### 4.5 云存储上传 + +| 功能 | 说明 | +|------|------| +| 上传到 Telegram 聊天 | 下载完成后转发到指定 Telegram 聊天/频道 | +| Rclone 上传 | 支持所有 Rclone 兼容的云存储(Google Drive、OneDrive、S3、阿里云盘等) | +| 阿里云盘(Aligo)| 专用适配器,支持阿里云盘 API | +| 上传前压缩 | 可配置上传前对文件进行 ZIP 压缩 | +| 上传后删除本地 | 上传成功后自动删除本地文件,节省磁盘空间 | + +### 4.6 多语言支持 + +支持界面/日志语言:英文(EN)、中文(ZH)、俄文(RU)、乌克兰文(UA),通过 `config.yaml` 中 `language` 字段配置。 + +### 4.7 容器化部署 + +提供完整 Dockerfile 和 docker-compose.yaml,支持: +- 单命令部署 `docker-compose up -d` +- 数据卷挂载(配置文件、下载目录) +- 端口映射(Web 界面) + +--- + +## 5. 项目架构 + +### 5.1 技术栈 + +| 层级 | 技术 | +|------|------| +| Telegram 通信 | pyrogram(自定义 fork,支持最新 TDLib) | +| 异步框架 | asyncio(Python 原生) | +| Web 服务 | Flask 2.2.2 + Flask-Login | +| 配置存储 | YAML(ruamel.yaml 保持格式) | +| 过滤器引擎 | PLY(Python Lex-Yacc) | +| 前端 | 原生 HTML/CSS/JS(无框架,深色主题) | +| 加密 | pycryptodome(AES-128-CBC) | +| 日志 | loguru + rich | +| 云存储 | Rclone(子进程调用)、aligo | + +### 5.2 整体架构图 + +``` +┌─────────────────────────────────────────────────────────┐ +│ media_downloader.py │ +│ (主进程入口) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ +│ │ Application │ │ asyncio │ │ Flask Web │ │ +│ │ 配置管理 │ │ 事件循环 │ │ Server │ │ +│ └──────────────┘ └──────┬───────┘ └───────────────┘ │ +│ │ │ +│ ┌────────────────┼──────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌────────────┐ ┌──────────────┐ │ +│ │ 下载主流程 │ │ Worker │ │ Bot 处理 │ │ +│ │ (每个聊天) │ │ 队列处理 │ │ (可选) │ │ +│ └──────┬──────┘ └─────┬──────┘ └──────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────┐ │ +│ │ pyrogram.Client │ │ +│ │ (Telegram MTProto API) │ │ +│ └────────────────┬────────────────┘ │ +└────────────────────┼────────────────────────────────────┘ + │ + ┌──────┴──────┐ + │ Telegram │ + │ 服务器 │ + └─────────────┘ +``` + +### 5.3 核心数据流 + +``` +程序启动 + │ + ├─ 加载 config.yaml → Application 初始化 + │ + ├─ 启动 Flask Web Server(端口 5001) + │ + ├─ 建立 Telegram 连接(pyrogram.Client.start()) + │ └─ 首次运行:提示手机号 + 验证码(终端交互) + │ + └─ download_all_chat() 主循环 + │ + ├─ 对每个 chat(config.yaml 中的 chat 列表): + │ ├─ get_chat_history_v2() → 逐批获取历史消息(每批 100 条) + │ ├─ exec_filter() → 应用过滤表达式 + │ ├─ 通过时 → add_download_task() → 压入 asyncio.Queue + │ └─ 记录 last_read_message_id + │ + └─ Worker 并发处理队列 + ├─ _get_media_meta() → 提取文件名、大小等元数据 + ├─ 检查文件是否已存在 → 跳过 + ├─ download_media() → 下载到 temp/ 目录 + ├─ _check_download_finish() → 校验完整性 + ├─ _move_to_download_path() → 移动到最终路径 + ├─ [可选] 上传到 Telegram 或云盘 + └─ update_download_status() → 更新 Web 统计数据 + +程序结束 + ├─ 等待所有队列任务完成 + ├─ 更新 config.yaml 中的 last_read_message_id + └─ 如 restart_program=True → 重启进程(os.execv) +``` + +--- + +## 6. 目录结构 + +``` +telegram_media_downloader/ +├── media_downloader.py # 主入口,程序启动和下载编排 +├── config.yaml # 用户配置文件(核心) +├── requirements.txt # Python 依赖列表 +├── setup.py # 打包配置 +├── Makefile # 快捷命令(install/test/lint) +├── Dockerfile # Docker 镜像构建文件 +├── docker-compose.yaml # Docker 编排配置 +├── gen_filter_cache.py # 过滤器缓存预生成脚本 +├── README.md # 英文文档 +├── README_CN.md # 中文文档 +├── PRD.md # 本文档 +│ +├── module/ # 核心业务模块 +│ ├── __init__.py +│ ├── app.py # Application 类、任务节点、配置管理 +│ ├── web.py # Flask Web 服务、所有 HTTP API +│ ├── bot.py # Telegram Bot 处理逻辑 +│ ├── filter.py # PLY 词法/语法分析过滤引擎 +│ ├── download_stat.py # 下载速度统计、任务进度追踪 +│ ├── get_chat_history_v2.py # 聊天历史异步生成器(优化版) +│ ├── pyrogram_extension.py # pyrogram 扩展(钩子、进度、上传) +│ ├── send_media_group_v2.py # 媒体组批量转发 +│ ├── cloud_drive.py # 云盘上传适配器(Rclone/Aligo) +│ ├── language.py # 多语言翻译字典 +│ ├── parsetab.py # PLY 自动生成的解析表(勿手动修改) +│ ├── parser.out # PLY 解析调试信息 +│ ├── templates/ # Flask HTML 模板 +│ │ ├── index.html # 主界面(含向导、下载控制) +│ │ ├── login.html # 登录页面 +│ │ └── control.html # 旧版控制页面(已由 index.html 统一) +│ └── static/ # Web 静态资源 +│ ├── css/ +│ ├── layui/ # Layui 前端框架(旧版残留) +│ ├── request/ # JS 请求库 +│ ├── login/ # 登录页样式 +│ └── aes/ # AES 加密 JS 库 +│ +├── utils/ # 通用工具类库 +│ ├── __init__.py # 版本号定义(__version__ = "2.2.5") +│ ├── crypto.py # AES-128-CBC 加密解密 +│ ├── file_management.py # 文件操作辅助函数 +│ ├── format.py # 字节格式化、日期时间处理、URL 解析 +│ ├── log.py # werkzeug 日志过滤器 +│ ├── meta.py # 元数据打印工具 +│ ├── meta_data.py # MetaData 类(过滤器属性集合) +│ ├── platform.py # 跨平台(Windows/Linux/macOS)检测 +│ └── updates.py # 版本更新检查 +│ +├── tests/ # 单元测试 +│ ├── test_common.py +│ ├── test_media_downloader.py +│ ├── module/ +│ │ └── test_app.py +│ └── utils/ +│ ├── test_cypto.py +│ ├── test_filter.py +│ ├── test_file_management.py +│ ├── test_format.py +│ ├── test_log.py +│ ├── test_meta.py +│ └── test_updates.py +│ +└── temp/ # 下载临时目录(运行时自动创建) +``` + +--- + +## 7. 核心模块详解 + +### 7.1 `media_downloader.py` — 主入口 + +**职责:** 程序生命周期管理、异步任务编排、下载流程控制。 + +| 函数 | 说明 | +|------|------| +| `main()` | 程序主入口,创建 pyrogram 客户端,启动 Web/Bot 服务,进入下载主循环 | +| `run_with_restart()` | 包装 main(),支持程序自动重启(`_app.restart_program = True` 时触发) | +| `download_all_chat()` | 遍历 config 中所有聊天,依次调用 `download_chat_task` | +| `download_chat_task()` | 单个聊天的完整下载流程:获取历史 → 过滤 → 入队 | +| `download_task()` | 处理单条消息的下载:元数据提取 → 去重 → 下载 → 移动 → 上传 | +| `download_media()` | 调用 pyrogram API 执行实际文件下载,写入 temp/ | +| `worker()` | asyncio Queue 消费者,循环从队列取任务并调用 `download_task` | +| `add_download_task()` | 将消息包装为任务节点,压入 asyncio.Queue | +| `_get_media_meta()` | 从 pyrogram Message 对象提取文件名、大小、扩展名等元数据 | +| `save_msg_to_file()` | 将纯文本消息保存为 .txt 文件(需配置 `enable_download_txt`) | + +**重启机制:** +```python +# 任意地方设置此标志: +_app.restart_program = True + +# run_with_restart() 检测到此标志后执行: +os.execv(sys.executable, [sys.executable] + sys.argv) +``` + +--- + +### 7.2 `module/app.py` — 应用配置管理 + +**职责:** 配置文件的读取/写入、任务节点数据结构定义、过滤器执行。 + +#### 核心枚举 + +```python +class DownloadState(Enum): + Downloading = 1 # 正在下载 + StopDownload = 2 # 已停止(暂停) + +class DownloadStatus(Enum): + SkipDownload = 1 # 已跳过(文件已存在) + SuccessDownload = 2 # 下载成功 + FailedDownload = 3 # 下载失败 + Downloading = 4 # 下载中 +``` + +#### 核心类 + +**`ChatDownloadConfig`** — 单个聊天的下载配置: + +| 属性 | 类型 | 说明 | +|------|------|------| +| `chat_id` | str/int | 聊天标识符 | +| `last_read_message_id` | int | 最后读取的消息ID(断点续传依据) | +| `download_filter` | str | 下载过滤表达式 | +| `ids_to_retry` | list | 失败需重试的消息ID列表 | + +**`TaskNode`** — 单次下载任务的运行时状态: + +| 属性 | 类型 | 说明 | +|------|------|------| +| `chat_id` | str/int | 所属聊天 | +| `total_task` | int | 本次任务总消息数 | +| `download_status` | dict | {message_id: DownloadStatus} | +| `is_running` | bool | 是否正在运行 | +| `upload_success_count` | int | 上传成功计数 | + +**`Application`** — 全局应用实例: + +| 属性 | 说明 | +|------|------| +| `config` | 完整的 YAML 配置字典 | +| `chat_download_config` | {chat_id: ChatDownloadConfig} | +| `save_path` | 下载根目录路径 | +| `api_id / api_hash` | Telegram API 凭证 | +| `media_types` | 要下载的媒体类型列表 | +| `file_formats` | 各媒体类型的格式白名单 | +| `file_path_prefix` | 目录结构组件列表 | +| `file_name_prefix` | 文件名前缀组件列表 | +| `proxy` | 代理配置字典 | +| `restart_program` | 重启标志(设为 True 触发重启) | +| `cloud_drive_config` | 云盘配置 | + +--- + +### 7.3 `module/web.py` — Web 服务 + +**职责:** Flask HTTP 服务、用户认证、前端 API、配置管理、频道历史。 + +**初始化方式(依赖注入):** + +```python +def init_web(app, client, add_download_task_func, download_chat_task_func): + """ + 由 media_downloader.py 在启动时调用,注入全局依赖: + - app: Application 实例 + - client: pyrogram.Client 实例 + - add_download_task_func: 添加下载任务的回调 + - download_chat_task_func: 下载聊天任务的回调 + """ +``` + +**频道历史存储:** 保存在 `module/channel_history.json`,结构如下: + +```json +[ + { + "chat_id": "example_channel", + "chat_title": "示例频道", + "chat_type": "CHANNEL", + "last_used": "2026-04-06T12:00:00" + } +] +``` + +**登录认证:** +- 密码在 `config.yaml` 的 `web_login_secret` 字段配置(明文,传输时 AES 加密) +- 未配置 `web_login_secret` 时自动跳过登录(本地开发模式) +- 使用 Flask-Login 管理会话 + +--- + +### 7.4 `module/filter.py` — 过滤引擎 + +**职责:** 将过滤表达式字符串解析并编译为可执行的过滤函数。 + +**技术实现:** 使用 PLY(Python Lex-Yacc)构建词法分析器和语法分析器,将表达式 AST 转换为 Python 可调用对象。 + +**支持的可过滤属性(`utils/meta_data.py` 中 `MetaData` 类定义):** + +| 属性名 | 数据类型 | 说明 | +|--------|---------|------| +| `message_date` | datetime | 消息发送时间 | +| `message_id` | int | 消息 ID | +| `media_file_size` | int | 文件大小(字节) | +| `media_width` | int | 图片/视频宽度(像素) | +| `media_height` | int | 图片/视频高度(像素) | +| `media_file_name` | str | 文件名(含扩展名) | +| `message_caption` | str | 消息说明文字 | +| `media_duration` | int | 视频/音频时长(秒) | +| `sender_id` | int | 发送者用户 ID | +| `sender_name` | str | 发送者名称 | +| `reply_to_message_id` | int | 被回复的消息 ID | + +--- + +### 7.5 `module/download_stat.py` — 下载统计 + +**职责:** 全局下载速度计算、任务进度数据的维护(供 Web API 消费)。 + +**核心全局数据(`_download_result`):** + +```python +_download_result: dict = { + "chat_id_1": { + message_id_1: { + "down_byte": 已下载字节数(int), + "total_size": 文件总字节数(int), + "file_name": 完整文件路径(str), + "download_speed": 当前速度(bytes/s,int), + "start_time": 开始时间戳(float), + } + } +} +``` + +**任务进度数据(`_task_progress`):** + +```python +_task_progress: dict = { + "current_chat": 当前处理的聊天 ID(str), + "current_chat_title": 当前聊天标题(str), + "checked_messages": 已检查的消息总数(int), + "skipped_files": 已跳过的文件数(int), + "downloading_files": 正在下载的文件数(int), + "completed_files": 已完成的文件数(int), + "failed_files": 失败的文件数(int), + "is_checking": 是否正在扫描消息(bool), +} +``` + +--- + +### 7.6 `module/pyrogram_extension.py` — pyrogram 扩展 + +**职责:** 扩展 pyrogram 客户端功能,包括下载进度回调、上传功能、权限检查。 + +**核心功能:** + +1. **`HookClient`** — 继承 `pyrogram.Client`,支持自定义钩子: + - `on_download_start` — 下载开始时回调 + - `on_download_progress` — 下载进度更新回调 + - `on_download_finish` — 下载完成回调 + +2. **`record_download_status`** — 装饰器,自动将下载进度写入 `download_stat.py` 的统计数据。 + +3. **上传功能:** + - `upload_telegram_chat()` — 上传文件到指定 Telegram 聊天 + - 通过 cloud_drive.py 上传到云存储 + +4. **`get_extension(file_id, mime_type)`** — 根据 Telegram 文件 ID 或 MIME 类型推断文件扩展名。 + +5. **`check_user_permission(user_id)`** — 检查用户是否在白名单中(Bot 模式权限控制)。 + +--- + +### 7.7 `module/cloud_drive.py` — 云盘上传 + +**支持适配器:** + +| 适配器 | 说明 | 配置字段 | +|--------|------|---------| +| `rclone` | 通用云存储(100+ 服务) | `upload_adapter: rclone` | +| `aligo` | 阿里云盘专用 | `upload_adapter: aligo` | + +**配置示例(config.yaml):** + +```yaml +cloud_drive_config: + enable_upload_file: true + upload_adapter: rclone # 或 aligo + remote_dir: "gdrive:/telegram" # Rclone 远程路径 + before_upload_file_zip: false # 上传前是否压缩 + after_upload_file_delete: true # 上传成功后删除本地文件 +``` + +--- + +## 8. 配置项完整说明 + +配置文件路径:`config.yaml`(项目根目录) + +### 8.1 必填项 + +| 字段 | 类型 | 示例 | 说明 | +|------|------|------|------| +| `api_id` | int | `12345678` | Telegram API ID,从 my.telegram.org 获取 | +| `api_hash` | str | `"abcdef1234..."` | Telegram API Hash,32位字符串 | +| `chat` | list | 见下方 | 下载目标配置列表 | +| `save_path` | str | `"/Users/xx/Downloads/tg"` | 文件保存根目录(绝对路径) | + +### 8.2 聊天配置(`chat` 列表元素) + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `chat_id` | str/int | — | 频道/群组用户名(`@xxx`或`xxx`)或数字 ID | +| `last_read_message_id` | int | `0` | 上次读取到的消息 ID,断点续传依据,程序自动更新 | +| `download_filter` | str | 无 | 过滤表达式,见第9节 | + +### 8.3 媒体类型和格式 + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `media_types` | list | `[audio, photo, video, document]` | 要下载的媒体类型 | +| `file_formats.audio` | list | `[all]` | 允许的音频格式,`all` 不限制 | +| `file_formats.video` | list | `[all]` | 允许的视频格式 | +| `file_formats.document` | list | `[all]` | 允许的文档格式 | + +**有效媒体类型值:** `audio`、`photo`、`video`、`document`、`voice`、`video_note`、`animation` + +### 8.4 文件路径配置 + +| 字段 | 类型 | 示例 | 说明 | +|------|------|------|------| +| `file_path_prefix` | list | `[chat_title, media_datetime]` | 目录层级结构组件,顺序即目录嵌套顺序 | +| `file_name_prefix` | list | `[message_id]` | 文件名前缀组件 | + +**`file_path_prefix` 可用值:** +- `chat_title` — 频道/群组名称 +- `media_datetime` — 媒体发布的年月(格式 `2024_01`) +- `media_type` — 媒体类型(audio/video/photo/document) + +**`file_name_prefix` 可用值:** +- `message_id` — 消息 ID +- `caption` — 消息说明文字(截断处理) +- `chat_title` — 聊天标题 + +### 8.5 Web 服务配置 + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `web_host` | str | `"127.0.0.1"` | Web 服务绑定地址,服务器部署改为 `"0.0.0.0"` | +| `web_port` | int | `5000` | Web 服务端口 | +| `web_login_secret` | str | 无(跳过登录) | Web 界面登录密码,AES 加密传输 | + +### 8.6 代理配置 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `proxy.scheme` | str | 代理类型:`socks5` 或 `http` | +| `proxy.hostname` | str | 代理服务器地址,本地代理填 `"127.0.0.1"` | +| `proxy.port` | int | 代理端口(Clash SOCKS5: 7891,HTTP: 7890;V2Ray: 10808/10809) | + +**常见代理软件对应配置:** + +| 代理软件 | SOCKS5 端口 | HTTP 端口 | 推荐配置 | +|---------|------------|----------|---------| +| Clash | 7891 | 7890 | `scheme: socks5, port: 7891` | +| 狗子云(VPN模式) | 无 | 7890 | `scheme: http, port: 7890` | +| V2Ray | 10808 | 10809 | `scheme: socks5, port: 10808` | +| Shadowsocks | 1080 | — | `scheme: socks5, port: 1080` | + +### 8.7 性能配置 + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `max_download_task` | int | `5` | 最大并发下载任务数 | +| `max_concurrent_transmissions` | int | `1` | 单个文件的并发分片传输数 | + +### 8.8 其他配置 + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `language` | str | `"EN"` | 界面语言:`EN`/`ZH`/`RU`/`UA` | +| `log_level` | str | `"INFO"` | 日志级别:`DEBUG`/`INFO`/`WARNING`/`ERROR` | +| `enable_download_txt` | bool | `false` | 是否将纯文本消息保存为 .txt 文件 | +| `hide_file_name` | bool | `false` | Web 界面是否隐藏下载文件名(隐私模式) | +| `bot_token` | str | 无 | Telegram Bot Token,启用 Bot 模式 | +| `allowed_user_ids` | list | 无(不限制) | 允许使用 Bot 的用户 ID 白名单 | + +--- + +## 9. 下载过滤器语法 + +过滤器配置在 `config.yaml` 的 `chat[i].download_filter` 字段中。 + +### 9.1 基本语法 + +``` +<属性名> <操作符> <值> [and/or <条件2>] +``` + +### 9.2 操作符 + +| 操作符 | 适用类型 | 示例 | +|--------|---------|------| +| `>=` | 数字、日期 | `media_file_size >= 1024` | +| `<=` | 数字、日期 | `message_date <= 2024-12-31 23:59:59` | +| `>` | 数字、日期 | `media_width > 1920` | +| `<` | 数字、日期 | `media_duration < 3600` | +| `==` | 数字、字符串 | `sender_id == 123456` | +| `!=` | 数字、字符串 | `sender_id != 123456` | +| `matches` | 字符串 | `media_file_name matches ".*\.mp4$"` | +| `contains` | 字符串 | `message_caption contains "4K"` | + +### 9.3 逻辑组合 + +``` +# AND:两个条件都满足 +message_date >= 2024-01-01 00:00:00 and message_date <= 2024-12-31 23:59:59 + +# OR:任一条件满足 +media_file_name matches ".*\.mp4$" or media_file_name matches ".*\.mkv$" + +# 复合条件 +message_date >= 2024-01-01 00:00:00 and media_file_size >= 10485760 +``` + +### 9.4 完整示例 + +```yaml +# 示例1:只下载 2024 年的内容 +download_filter: message_date >= 2024-01-01 00:00:00 and message_date <= 2024-12-31 23:59:59 + +# 示例2:只下载大于 10MB 的文件 +download_filter: media_file_size >= 10485760 + +# 示例3:只下载 1080p 及以上的视频 +download_filter: media_width >= 1920 and media_height >= 1080 + +# 示例4:只下载文件名包含特定字符串的文件 +download_filter: media_file_name matches ".*\\.720p.*" + +# 示例5:只下载标题含关键字的消息 +download_filter: message_caption contains "完整版" + +# 示例6:下载特定时间段内的大文件视频 +download_filter: message_date >= 2023-06-01 00:00:00 and media_file_size >= 52428800 and media_duration >= 600 +``` + +### 9.5 日期格式规范 + +日期必须使用格式:`YYYY-MM-DD HH:MM:SS`,例如: +- `2024-01-18 00:00:00`(起始日期,时间设 00:00:00) +- `2024-12-31 23:59:59`(结束日期,时间设 23:59:59) + +--- + +## 10. Web 界面 API 接口 + +Base URL:`http://127.0.0.1:5001`(默认,端口可配置) + +### 10.1 无需认证的接口 + +#### `GET /api/setup_status` +检测当前配置完成度,用于判断是否需要显示新用户引导向导。 + +**响应:** +```json +{ + "has_api_credentials": true, // api_id 和 api_hash 是否已配置且有效 + "has_session": true, // 是否已有 Telegram 登录会话文件 + "has_chat": true, // 是否已配置了至少一个聊天 + "proxy": { + "scheme": "http", + "hostname": "127.0.0.1", + "port": 7890 + }, + "save_path": "/Users/xx/Downloads/tg" +} +``` + +#### `POST /api/save_initial_config` +保存初始配置(新用户引导第3步使用),无需登录。 + +**请求体:** +```json +{ + "api_id": 12345678, + "api_hash": "abcdef...", + "proxy_enabled": true, + "proxy_scheme": "http", + "proxy_hostname": "127.0.0.1", + "proxy_port": 7890, + "save_path": "/Users/xx/Downloads/tg" +} +``` + +**响应:** +```json +{ "success": true, "message": "配置已保存,程序将重启" } +``` + +#### `GET /get_app_version` +**响应:** `"2.2.5"`(纯文本) + +--- + +### 10.2 需要认证的接口 + +所有以下接口在设置了 `web_login_secret` 时需要先登录,否则返回 302 重定向到 `/login`。 + +#### `GET /get_download_status` +获取实时下载速度和状态。 + +**响应:** +```json +{ + "download_speed": "1.23 MB/s", + "upload_speed": "0.00 B/s", + "state": "pause" // "pause" = 下载中,"continue" = 已暂停 +} +``` + +--- + +#### `GET /api/task_progress` +获取当前任务详细进度。 + +**响应:** +```json +{ + "current_chat": "example_channel", + "current_chat_title": "示例频道", + "checked_messages": 1234, + "skipped_files": 567, + "downloading_files": 3, + "completed_files": 890, + "failed_files": 2, + "is_checking": false +} +``` + +--- + +#### `POST /set_download_state?state=pause` +暂停或继续下载。 + +| state 参数 | 效果 | +|-----------|------| +| `pause` | 暂停下载 | +| `continue` | 继续下载 | + +**响应:** `"pause"` 或 `"continue"`(纯文本,表示操作后的新状态) + +--- + +#### `GET /api/get_config` +获取当前配置信息(供 Web 界面初始化表单)。 + +**响应:** +```json +{ + "chat_id": "example_channel", + "download_filter": "message_date >= 2024-01-01 00:00:00", + "save_path": "/Users/xx/Downloads/tg" +} +``` + +--- + +#### `POST /api/validate_chat` +验证频道/群组是否可访问(连通 Telegram 服务器查询)。 + +**请求体:** +```json +{ "chat_id": "example_channel" } +``` + +**响应(成功):** +```json +{ + "valid": true, + "chat_id": "example_channel", + "chat_title": "示例频道", + "chat_type": "CHANNEL" // CHANNEL / SUPERGROUP / GROUP / PRIVATE +} +``` + +**响应(失败):** +```json +{ + "valid": false, + "error": "Chat not found" +} +``` + +--- + +#### `POST /api/save_path` +修改文件保存路径(立即生效,无需重启)。 + +**请求体:** +```json +{ "save_path": "/new/save/path" } +``` + +**响应:** +```json +{ "success": true, "save_path": "/new/save/path" } +``` + +--- + +#### `POST /api/save_and_restart` +保存下载配置并重启程序(最常用操作)。 + +**请求体:** +```json +{ + "chat_id": "example_channel", + "start_date": "2024-01-01", + "end_date": "2024-12-31", // 可为空 + "chat_title": "示例频道", + "chat_type": "CHANNEL" +} +``` + +**响应:** +```json +{ "success": true, "message": "配置已保存,程序正在重启" } +``` + +--- + +#### `POST /api/start_download` +动态启动下载任务(**不重启程序**,直接将任务加入当前运行队列)。 + +**请求体(同 save_and_restart)** + +**响应:** +```json +{ "success": true, "message": "下载任务已启动" } +``` + +--- + +#### `GET /api/channel_history` +获取频道历史记录列表(按最近使用排序)。 + +**响应:** +```json +{ + "success": true, + "history": [ + { + "chat_id": "example_channel", + "chat_title": "示例频道", + "chat_type": "CHANNEL", + "last_used": "2026-04-06T12:00:00" + } + ] +} +``` + +--- + +#### `DELETE /api/channel_history/` +删除指定频道的历史记录。 + +**响应:** `{ "success": true, "message": "已删除" }` + +--- + +#### `POST /api/channel_history/clear` +清空所有频道历史记录。 + +**响应:** `{ "success": true, "message": "已清空" }` + +--- + +#### `GET /get_download_list?already_down=false` +获取下载文件列表。 + +| 参数 | 值 | 说明 | +|-----|-----|------| +| `already_down` | `false` | 返回正在下载的文件列表 | +| `already_down` | `true` | 返回已完成下载的文件列表 | + +**响应(JSON 数组):** +```json +[ + { + "chat": "example_channel", + "id": "12345", + "filename": "video.mp4", + "total_size": "1.23 GB", + "download_progress": "45.6", + "download_speed": "2.34 MB/s", + "save_path": "/Users/xx/Downloads/tg/示例频道/2024_01/video.mp4" + } +] +``` + +--- + +## 11. 数据结构 + +### 11.1 `config.yaml` 完整示例 + +```yaml +# ── Telegram API 凭证(必填)── +api_id: 12345678 +api_hash: abcdef1234567890abcdef1234567890 + +# ── 下载目标列表(至少一个)── +chat: + - chat_id: example_channel + last_read_message_id: 0 # 程序运行后自动更新,勿手动修改 + download_filter: message_date >= 2024-01-01 00:00:00 + +# ── 媒体类型 ── +media_types: + - audio + - photo + - video + - document + - voice + - video_note + +# ── 文件格式过滤 ── +file_formats: + audio: + - all + document: + - all + video: + - all + +# ── 目录/文件名结构 ── +file_path_prefix: + - chat_title + - media_datetime + +# ── 保存路径(必填)── +save_path: /Users/username/Downloads/telegram_downloads + +# ── Web 界面 ── +web_host: 127.0.0.1 +web_port: 5001 +# web_login_secret: your_password # 注释掉则不需要密码 + +# ── 代理(大陆必填)── +proxy: + scheme: http # 或 socks5 + hostname: 127.0.0.1 + port: 7890 + +# ── 可选高级配置 ── +language: ZH +max_download_task: 5 +enable_download_txt: false + +# ── Bot 模式(可选)── +# bot_token: 1234567890:AAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# allowed_user_ids: [123456789] + +# ── 云盘上传(可选)── +# cloud_drive_config: +# enable_upload_file: true +# upload_adapter: rclone +# remote_dir: "gdrive:/Telegram" +# after_upload_file_delete: true +``` + +### 11.2 `channel_history.json` 结构 + +```json +[ + { + "chat_id": "example_channel", + "chat_title": "示例频道", + "chat_type": "CHANNEL", + "last_used": "2026-04-06T12:34:56.789000" + } +] +``` + +--- + +## 12. 下载目录结构 + +下载文件的存储结构由 `file_path_prefix` 和 `file_name_prefix` 两个配置项控制。 + +### 12.1 默认结构(`file_path_prefix: [chat_title, media_datetime]`) + +``` +save_path/ +├── 示例频道/ ← chat_title +│ ├── 2024_01/ ← media_datetime (YYYY_MM) +│ │ ├── 1001 - video_001.mp4 ← message_id - 文件名 +│ │ ├── 1002 - photo_001.jpg +│ │ └── 1003 - document.pdf +│ └── 2024_02/ +│ └── ... +├── 另一个频道/ +│ └── ... +└── temp/ ← 下载中的临时文件 + └── ...(下载完成后自动清理) +``` + +### 12.2 按媒体类型分目录(`file_path_prefix: [chat_title, media_type]`) + +``` +save_path/ +└── 示例频道/ + ├── video/ + │ ├── video_001.mp4 + │ └── video_002.mkv + ├── photo/ + │ └── photo_001.jpg + ├── audio/ + └── document/ +``` + +--- + +## 13. 安装与运行 + +### 13.1 环境要求 + +- Python 3.7+ +- 网络可访问 Telegram 服务器(大陆需代理) +- 已申请 Telegram API 凭证 + +### 13.2 本地安装 + +```bash +# 克隆仓库 +git clone https://github.com/tangyoha/telegram_media_downloader.git +cd telegram_media_downloader + +# 安装依赖 +pip3 install -r requirements.txt +# 或使用 Makefile +make install + +# 编辑配置文件(必须) +# 填写 api_id, api_hash, save_path, chat(或使用 Web 向导) +nano config.yaml + +# 启动 +python3 media_downloader.py +``` + +### 13.3 首次运行登录流程 + +``` +程序启动 + ↓ +提示输入手机号:Enter phone number: +86138... + ↓ +提示输入验证码(Telegram App 内收件箱):Enter OTP: 12345 + ↓ +(可能需要)输入两步验证密码:Enter password: **** + ↓ +登录成功,生成 session 文件(项目根目录,*.session) + ↓ +后续运行无需重新登录(session 文件保存登录状态) +``` + +**注意:** session 文件是登录凭证,请妥善保管,不要提交到代码仓库。 + +### 13.4 Docker 部署 + +```bash +# 方式一:使用 docker-compose(推荐) +# 修改 docker-compose.yaml 中的卷挂载路径 +docker-compose up -d + +# 方式二:手动运行容器 +docker pull tangyoha/telegram_media_downloader:latest +docker run -d \ + -p 5000:5000 \ + -v $(pwd)/config.yaml:/app/config.yaml \ + -v /your/download/path:/downloads \ + tangyoha/telegram_media_downloader:latest +``` + +### 13.5 访问 Web 界面 + +启动后访问:`http://localhost:5001`(端口以 config.yaml 中 `web_port` 为准) + +--- + +## 14. 依赖说明 + +### 14.1 核心依赖 + +| 库 | 版本 | 用途 | 说明 | +|----|------|------|------| +| pyrogram | 自定义 fork | Telegram MTProto 客户端 | 使用 tangyoha 的修补版,修复了若干上游问题 | +| PyTgCrypto | 1.2.6 | Telegram 加密 | pyrogram 的加密加速依赖 | +| ruamel.yaml | 0.17.21 | YAML 读写 | 保持 YAML 注释和格式,用于写回 config.yaml | +| PyYAML | 5.3.1 | YAML 解析 | 兼容性保留 | +| flask | 2.2.2 | Web 框架 | 提供 HTTP 服务和模板渲染 | +| flask-login | 0.6.2 | 会话认证 | Web 界面登录管理 | +| Werkzeug | 2.2.2 | WSGI 工具 | Flask 依赖,HTTP 服务底层 | +| pycryptodome | 3.18.0 | AES 加密 | Web 密码加密传输 | +| ply | 3.11 | 词法/语法分析 | 下载过滤器表达式引擎 | +| loguru | 0.6.0 | 日志 | 结构化彩色日志输出 | +| rich | 12.5.1 | 终端输出 | 彩色进度显示 | +| requests | 2.32.3 | HTTP 请求 | 版本更新检查 | + +### 14.2 可选依赖 + +| 库 | 用途 | +|----|------| +| rclone | 云存储上传(外部命令,非 Python 包) | +| aligo | 阿里云盘上传 | + +--- + +## 15. 当前限制与已知问题 + +### 15.1 功能限制 + +| 限制 | 描述 | 影响 | +|------|------|------| +| 终端首次登录 | 首次运行必须在终端手动输入手机号+验证码,不支持纯 Web 引导完成登录 | 服务器无头部署不便 | +| 无内置调度 | 没有定时任务功能,需借助外部 cron 实现周期性运行 | 无法自动增量同步 | +| 频道列表硬编码 | config.yaml 中的 chat 列表需手动编辑或通过 Web 界面覆盖,不支持多频道并存管理 | 多频道管理不便 | +| 单进程架构 | 所有下载在一个进程中,重启会中断当前任务 | 更新配置代价大 | +| 无下载队列持久化 | 程序重启后,未完成的队列任务丢失 | 中断恢复能力弱 | +| Web 界面无 HTTPS | Web 服务仅 HTTP,暴露在公网有安全风险 | 服务器部署需额外加固 | + +### 15.2 已知技术问题 + +| 问题 | 原因 | 临时解决方案 | +|------|------|------------| +| 代理配置敏感 | pyrogram 直连 Telegram DC IP,不走系统代理,必须在 config.yaml 中显式配置代理 | 确保代理软件开启了 SOCKS5/HTTP 端口 | +| Flask 模板缓存 | 非 debug 模式下 Jinja2 不自动重载模板,修改 HTML 后需重启服务 | 重启服务进程 | +| PLY 解析表文件 | `parsetab.py` 是自动生成的,不应手动修改,但提交到了版本库中 | 保持不修改此文件 | +| 私有频道下载受限 | 必须以账号身份加入该私有频道才能下载,Bot Token 无法访问普通私有频道 | 用账号加入频道后再配置下载 | + +### 15.3 性能特性 + +- 历史消息获取速度受 Telegram API 频率限制(大频道可能需要数分钟至数小时扫描) +- 并发下载数默认 5,可通过 `max_download_task` 调高,但过高可能触发 Telegram 限流 +- 临时文件在 `temp/` 目录,建议 `temp/` 和最终保存路径在同一磁盘分区(避免跨分区移动开销) + +--- + +## 16. 优化建议与迭代方向 + +### 16.1 高优先级(影响核心体验) + +#### 1. Web 引导完成 Telegram 登录 +**现状:** 首次登录必须在终端交互完成,对服务器部署和非技术用户极不友好。 +**建议:** 在 Web 向导第4步中,通过轮询 `/api/login_status` 接口感知登录状态,或通过 WebSocket 推送终端输出,在 Web 界面内完成手机号+验证码输入。 + +#### 2. 下载任务持久化 +**现状:** 程序重启后,正在进行的任务从队列消失(只有 `last_read_message_id` 之前的已读记录)。 +**建议:** 将 asyncio Queue 的待处理任务序列化到 SQLite 或 JSON 文件,重启后自动恢复。 + +#### 3. 多频道独立配置管理 +**现状:** `save_and_restart` 接口会覆盖 config.yaml 中的 `chat` 列表为单个频道,不支持同时配置多频道。 +**建议:** Web 界面支持频道列表管理,增删改每个频道的配置,而不是每次覆盖。 + +--- + +### 16.2 中优先级(提升使用效率) + +#### 4. 内置定时任务(增量同步调度器) +**建议:** 在 Web 界面添加"定时同步"功能(如每天凌晨 2 点自动运行),内置 APScheduler 或 cron 风格调度,无需依赖外部 cron。 + +#### 5. 下载统计与历史报告 +**建议:** 增加下载历史记录(SQLite 存储),Web 界面展示: +- 按日期的下载量图表 +- 按频道的下载统计 +- 磁盘空间使用情况 + +#### 6. 实时进度推送(WebSocket) +**现状:** Web 界面通过轮询(每 2 秒 fetch 一次)获取进度,有延迟且浪费请求。 +**建议:** 改用 WebSocket 或 SSE(Server-Sent Events)推送实时进度,降低延迟和服务器压力。 + +#### 7. 文件去重优化 +**现状:** 通过比较文件路径和大小判断是否已下载。 +**建议:** 增加基于消息 ID 的已下载记录(SQLite),即使文件被移动或重命名,也不会重复下载同一消息。 + +--- + +### 16.3 低优先级(长期演进) + +#### 8. HTTPS 支持 +**建议:** Web 服务支持 SSL/TLS 配置(提供证书路径配置项),或提供 Nginx 反向代理配置示例,供服务器部署时使用。 + +#### 9. 移动端适配 +**现状:** Web 界面在小屏幕(480px 以下)有布局问题。 +**建议:** 完善响应式布局,优化移动端操作体验(触摸友好的表单控件、大按钮)。 + +#### 10. 过滤器可视化编辑器 +**现状:** 过滤器需手动输入表达式字符串,语法不直观。 +**建议:** Web 界面提供图形化过滤条件构建器(类似 Notion filter),自动生成过滤表达式字符串。 + +#### 11. 多账号支持 +**建议:** 支持配置多个 Telegram 账号(多个 `api_id/api_hash`),不同频道使用不同账号下载,规避单账号频率限制。 + +#### 12. API 文档自动生成 +**建议:** 使用 Flask-RESTX 或 flasgger 为 Web API 生成 Swagger 文档,方便二次开发和集成。 + +--- + +### 16.4 代码质量建议 + +| 类别 | 建议 | +|------|------| +| 测试覆盖率 | 补充 web.py API 端点的集成测试,当前测试主要覆盖工具函数 | +| 类型注解 | 核心函数补充完整的 Python 类型注解(typing),提升 IDE 支持和可读性 | +| 配置校验 | 启动时对 config.yaml 进行 Schema 校验(使用 pydantic 或 jsonschema),提前发现配置错误 | +| 错误处理 | media_downloader.py 中部分 except 过于宽泛(`except Exception`),建议细化为具体异常类型 | +| 日志规范 | 统一使用 loguru,移除部分混用 print 和 logging 的情况 | +| 文档字符串 | 核心函数(尤其是 web.py 的路由函数)补充 docstring | + +--- + +*文档结束* + +> 本文档基于项目版本 2.2.5 编写(2026-04-06)。如项目代码有更新,部分细节可能与实际不符,请以代码为准。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8328891 --- /dev/null +++ b/README.md @@ -0,0 +1,291 @@ + +

Telegram Media Downloader

+ +

+Unittest +Coverage Status +License: MIT +Code style: black + +Code style: black +

+ +

+ 中文 · + Feature request + · + Report a bug + · + Support: Discussions + & + Telegram Community +

+ +## Overview +> Support two default running + +* The robot is running, and the command `download` or `forward` is issued from the robot + +* Download as a one-time download tool + +### UI + +#### Web page + +> After running, open a browser and visit `localhost:5000` +> If it is a remote machine, you need to configure web_host: 0.0.0.0 + + +Code style: black + +### Robot + +> Need to configure bot_token, please refer to [Documentation](https://github.com/tangyoha/telegram_media_downloader/wiki/How-to-Download-Using-Robots) + +Code style: black + +### Support + +| Category | Support | +| -------------------- | ------------------------------------------------ | +| Language | `Python 3.7` and above | +| Download media types | audio, document, photo, video, video_note, voice | + +### Version release plan + +* [v2.2.0](https://github.com/tangyoha/telegram_media_downloader/issues/2) + +## Installation + +For *nix os distributions with `make` availability + +```sh +git clone https://github.com/tangyoha/telegram_media_downloader.git +cd telegram_media_downloader +make install +``` + +For Windows which doesn't have `make` inbuilt + +```sh +git clone https://github.com/tangyoha/telegram_media_downloader.git +cd telegram_media_downloader +pip3 install -r requirements.txt +``` + +## Docker +> For more detailed installation tutorial, please check the wiki + +Make sure you have **docker** and **docker-compose** installed +```sh +docker pull tangyoha/telegram_media_downloader:latest +mkdir -p ~/app && mkdir -p ~/app/log/ && cd ~/app +wget https://raw.githubusercontent.com/tangyoha/telegram_media_downloader/master/docker-compose.yaml -O docker-compose.yaml +wget https://raw.githubusercontent.com/tangyoha/telegram_media_downloader/master/config.yaml -O config.yaml +wget https://raw.githubusercontent.com/tangyoha/telegram_media_downloader/master/data.yaml -O data.yaml +# vi config.yaml and docker-compose.yaml +vi config.yaml + +# The first time you need to start the foreground +# enter your phone number and code, then exit(ctrl + c) +docker-compose run --rm telegram_media_downloader + +# After performing the above operations, all subsequent startups will start in the background +docker-compose up -d + +# Upgrade +docker pull tangyoha/telegram_media_downloader:latest +cd ~/app +docker-compose down +docker-compose up -d +``` + +## Upgrade installation + +```sh +cd telegram_media_downloader +pip3 install -r requirements.txt +``` + +## Configuration + +All the configurations are passed to the Telegram Media Downloader via `config.yaml` file. + +**Getting your API Keys:** +The very first step requires you to obtain a valid Telegram API key (API id/hash pair): + +1. Visit [https://my.telegram.org/apps](https://my.telegram.org/apps) and log in with your Telegram Account. +2. Fill out the form to register a new Telegram application. +3. Done! The API key consists of two parts: **api_id** and **api_hash**. + +**Getting chat id:** + +**1. Using web telegram:** + +1. Open + +2. Now go to the chat/channel and you will see the URL as something like + - `https://web.telegram.org/?legacy=1#/im?p=u853521067_2449618633394` here `853521067` is the chat id. + - `https://web.telegram.org/?legacy=1#/im?p=@somename` here `somename` is the chat id. + - `https://web.telegram.org/?legacy=1#/im?p=s1301254321_6925449697188775560` here take `1301254321` and add `-100` to the start of the id => `-1001301254321`. + - `https://web.telegram.org/?legacy=1#/im?p=c1301254321_6925449697188775560` here take `1301254321` and add `-100` to the start of the id => `-1001301254321`. + +**2. Using bot:** + +1. Use [@username_to_id_bot](https://t.me/username_to_id_bot) to get the chat_id of + - almost any telegram user: send username to the bot or just forward their message to the bot + - any chat: send chat username or copy and send its joinchat link to the bot + - public or private channel: same as chats, just copy and send to the bot + - id of any telegram bot + +### config.yaml + +```yaml +api_hash: your_api_hash +api_id: your_api_id +chat: +- chat_id: telegram_chat_id + last_read_message_id: 0 + download_filter: message_date >= 2022-12-01 00:00:00 and message_date <= 2023-01-17 00:00:00 +- chat_id: telegram_chat_id_2 + last_read_message_id: 0 +# note we remove ids_to_retry to data.yaml +ids_to_retry: [] +media_types: +- audio +- document +- photo +- video +- voice +- animation #gif +file_formats: + audio: + - all + document: + - pdf + - epub + video: + - mp4 +save_path: D:\telegram_media_downloader +file_path_prefix: +- chat_title +- media_datetime +upload_drive: + # required + enable_upload_file: true + # required + remote_dir: drive:/telegram + # required + upload_adapter: rclone + # option,when config upload_adapter rclone then this config are required + rclone_path: D:\rclone\rclone.exe + # option + before_upload_file_zip: True + # option + after_upload_file_delete: True +hide_file_name: true +file_name_prefix: +- message_id +- file_name +file_name_prefix_split: ' - ' +max_download_task: 5 +web_host: 127.0.0.1 +web_port: 5000 +language: EN +web_login_secret: 123 +allowed_user_ids: +- 'me' +date_format: '%Y_%m' +enable_download_txt: false +``` + +- **api_hash** - The api_hash you got from telegram apps +- **api_id** - The api_id you got from telegram apps +- **bot_token** - Your bot token +- **chat** - Chat list + - `chat_id` - The id of the chat/channel you want to download media. Which you get from the above-mentioned steps. + - `download_filter` - Download filter, see [How to use Filter](https://github.com/tangyoha/telegram_media_downloader/wiki/How-to-use-Filter) + - `last_read_message_id` - If it is the first time you are going to read the channel let it be `0` or if you have already used this script to download media it will have some numbers which are auto-updated after the scripts successful execution. Don't change it. + - `ids_to_retry` - `Leave it as it is.` This is used by the downloader script to keep track of all skipped downloads so that it can be downloaded during the next execution of the script. +- **media_types** - Type of media to download, you can update which type of media you want to download it can be one or any of the available types. +- **file_formats** - File types to download for supported media types which are `audio`, `document` and `video`. Default format is `all`, downloads all files. +- **save_path** - The root directory where you want to store downloaded files. +- **file_path_prefix** - Store file subfolders, the order of the list is not fixed, can be randomly combined. + - `chat_title` - Channel or group title, it will be chat id if not exist title. + - `media_datetime` - Media date. + - `media_type` - Media type, also see `media_types`. +- **upload_drive** - You can upload file to cloud drive. + - `enable_upload_file` - Enable upload file, default `false`. + - `remote_dir` - Where you upload, like `drive_id/drive_name`. + - `upload_adapter` - Upload file adapter, which can be `rclone`, `aligo`. If it is `rclone`, it supports all `rclone` servers that support uploading. If it is `aligo`, it supports uploading `Ali cloud disk`. + - `rclone_path` - RClone exe path, see [How to use rclone](https://github.com/tangyoha/telegram_media_downloader/wiki/Rclone) + - `before_upload_file_zip` - Zip file before upload, default `false`. + - `after_upload_file_delete` - Delete file after upload success, default `false`. +- **file_name_prefix** - Custom file name, use the same as **file_path_prefix** + - `message_id` - Message id + - `file_name` - File name (may be empty) + - `caption` - The title of the message (may be empty) +- **file_name_prefix_split** - Custom file name prefix symbol, the default is `-` +- **max_download_task** - The maximum number of task download tasks, the default is 5. +- **hide_file_name** - Whether to hide the web interface file name, default `false` +- **web_host** - Web host +- **web_port** - Web port +- **language** - Application language, the default is English (`EN`), optional `ZH`(Chinese),`RU`,`UA` +- **web_login_secret** - Web page login password, if not configured, no login is required to access the web page +- **log_level** - see `logging._nameToLevel`. +- **forward_limit** - Limit the number of forwards per minute, the default is 33, please do not modify this parameter by default. +- **allowed_user_ids** - Who is allowed to use the robot? The default login account can be used. Please add single quotes to the name with @. +- **date_format** Support custom configuration of media_datetime format in file_path_prefix.see [python-datetime](https://docs.python.org/3/library/datetime.html) +- **enable_download_txt** Enable download txt file, default `false` + +## Execution + +```sh +python3 media_downloader.py +``` + +All downloaded media will be stored at the root of `save_path`. +The specific location reference is as follows: + +The complete directory of video download is: `save_path`/`chat_title`/`media_datetime`/`media_type`. +The order of the list is not fixed and can be randomly combined. +If the configuration is empty, all files are saved under `save_path`. + +## Proxy + +`socks4, socks5, http` proxies are supported in this project currently. To use it, add the following to the bottom of your `config.yaml` file + +```yaml +proxy: + scheme: socks5 + hostname: 127.0.0.1 + port: 1234 + username: your_username(delete the line if none) + password: your_password(delete the line if none) +``` + +If your proxy doesn’t require authorization you can omit username and password. Then the proxy will automatically be enabled. + +## Contributing + +### Contributing Guidelines + +Read through our [contributing guidelines](https://github.com/tangyoha/telegram_media_downloader/blob/master/CONTRIBUTING.md) to learn about our submission process, coding rules and more. + +### Want to Help? + +Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our guidelines for [contributing](https://github.com/tangyoha/telegram_media_downloader/blob/master/CONTRIBUTING.md). + +### Code of Conduct + +Help us keep Telegram Media Downloader open and inclusive. Please read and follow our [Code of Conduct](https://github.com/tangyoha/telegram_media_downloader/blob/master/CODE_OF_CONDUCT.md). + + +### Sponsor + +[PayPal](https://paypal.me/tangyoha?country.x=C2&locale.x=zh_XC) + +

+Code style: black +Code style: black +

diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..9d2e03d --- /dev/null +++ b/README_CN.md @@ -0,0 +1,292 @@ + +

电报资源下载

+ +

+Unittest +Coverage Status +License: MIT +Code style: black + +Code style: black + +

+ +

+ English · + 新功能请求 + · + 报告bug + · + 帮助: 讨论 + & + 电报讨论群 +

+ +## 概述 + +> 支持两种默认运行 + +* 机器人运行,从机器人下发命令`下载`或者`转发` + +* 作为一个一次性的下载工具下载 + +### 界面 + +#### 网页 + +> 运行后打开浏览器访问`localhost:5000` +> 如果是远程机器需要配置web_host: 0.0.0.0 + + +Code style: black + +### 机器人 + +> 需要配置bot_token,具体参考[文档](https://github.com/tangyoha/telegram_media_downloader/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E6%9C%BA%E5%99%A8%E4%BA%BA%E4%B8%8B%E8%BD%BD) + + +Code style: black + +### 支持 + +| 类别 | 支持 | +| ------------ | ---------------------------------------- | +| 语言 | `Python 3.7` 及以上 | +| 下载媒体类型 | 音频、文档、照片、视频、video_note、语音 | + +### 版本发布计划 + +* [v2.2.0](https://github.com/tangyoha/telegram_media_downloader/issues/2) + +## 安装 + +对于具有 `make` 可用性的 *nix 操作系统发行版 + +```sh +git clone https://github.com/tangyoha/telegram_media_downloader.git +cd telegram_media_downloader +make install +``` + +对于没有内置 `make` 的 Windows + +```sh +git clone https://github.com/tangyoha/telegram_media_downloader.git +cd telegram_media_downloader +pip3 install -r requirements.txt +``` +## Docker容器 +> 更详细安装教程请查看wiki + +确保安装了 **docker** 和 **docker-compose** +```sh +docker pull tangyoha/telegram_media_downloader:latest +mkdir -p ~/app && mkdir -p ~/app/log/ && cd ~/app +wget https://raw.githubusercontent.com/tangyoha/telegram_media_downloader/blob/master/docker-compose.yaml -O docker-compose.yaml +wget https://raw.githubusercontent.com/tangyoha/telegram_media_downloader/blob/master/config.yaml -O config.yaml +wget https://raw.githubusercontent.com/tangyoha/telegram_media_downloader/blob/master/data.yaml -O data.yaml +# vi config.yaml and docker-compose.yaml +vi config.yaml + +# 第一次需要前台启动 +# 输入你的电话号码和密码,然后退出(ctrl + c) +docker-compose run --rm telegram_media_downloader + +# 执行完以上操作后,后面的所有启动都在后台启动 +docker-compose up -d + +# 升级 +docker pull tangyoha/telegram_media_downloader:latest +cd ~/app +docker-compose down +docker-compose up -d +``` + +## 升级安装 + +```sh +cd telegram_media_downloader +pip3 install -r requirements.txt +``` + +## 配置 + +所有配置都通过 config.yaml 文件传递​​给 `Telegram Media Downloader`。 + +**获取您的 API 密钥:** +第一步需要您获得有效的 Telegram API 密钥(API id/hash pair): + +1. 访问 [https://my.telegram.org/apps](https://my.telegram.org/apps) 并使用您的 Telegram 帐户登录。 +2. 填写表格以注册新的 Telegram 应用程序。 +3. 完成! API 密钥由两部分组成:**api_id** 和**api_hash**。 + +**获取聊天ID:** +> 如果你需要下载收藏夹的内容请填`me` + +**1。使用网络电报:** + +1. 打开 +2. 现在转到聊天/频道,您将看到 URL 类似 + +- `https://web.telegram.org/?legacy=1#/im?p=u853521067_2449618633394` 这里 `853521067` 是聊天 ID。 +- `https://web.telegram.org/?legacy=1#/im?p=@somename` 这里的 `somename` 是聊天 ID。 +- `https://web.telegram.org/?legacy=1#/im?p=s1301254321_6925449697188775560` 此处取 `1301254321` 并将 `-100` 添加到 id => `-1001301254321` 的开头。 +- `https://web.telegram.org/?legacy=1#/im?p=c1301254321_6925449697188775560` 此处取 `1301254321` 并将 `-100` 添加到 id => `-1001301254321` 的开头。 + +**2。使用机器人:** +1.使用[@username_to_id_bot](https://t.me/username_to_id_bot)获取chat_id + - 几乎所有电报用户:将用户名发送给机器人或将他们的消息转发给机器人 + - 任何聊天:发送聊天用户名或复制并发送其加入聊天链接到机器人 + - 公共或私人频道:与聊天相同,只需复制并发送给机器人 + - 任何电报机器人的 ID + +### 配置文件 + +```yaml +api_hash: your_api_hash +api_id: your_api_id +bot_token: your_bot_token +chat: +- chat_id: telegram_chat_id + last_read_message_id: 0 + download_filter: message_date >= 2022-12-01 00:00:00 and message_date <= 2023-01-17 00:00:00 +- chat_id: telegram_chat_id_2 + last_read_message_id: 0 +# 我们将ids_to_retry移到data.yaml +ids_to_retry: [] +media_types: +- audio +- document +- photo +- video +- voice +- animation #gif +file_formats: + audio: + - all + document: + - pdf + - epub + video: + - mp4 +save_path: D:\telegram_media_downloader +file_path_prefix: +- chat_title +- media_datetime +upload_drive: + enable_upload_file: true + remote_dir: drive:/telegram + before_upload_file_zip: True + after_upload_file_delete: True +hide_file_name: true +file_name_prefix: +- message_id +- file_name +file_name_prefix_split: ' - ' +max_download_task: 5 +web_host: 127.0.0.1 +web_port: 5000 +web_login_secret: 123 +allowed_user_ids: +- 'me' +date_format: '%Y_%m' +enable_download_txt: false +``` + +- **api_hash** - 你从电报应用程序获得的 api_hash +- **api_id** - 您从电报应用程序获得的 api_id +- **bot_token** - 你的机器人凭证 +- **chat** - 多频道 + - `chat_id` - 您要下载媒体的聊天/频道的 ID。你从上述步骤中得到的。 + - `download_filter` - 下载过滤器, 查阅 [如何使用过滤器](https://github.com/tangyoha/telegram_media_downloader/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E8%BF%87%E6%BB%A4%E5%99%A8) + - `last_read_message_id` -如果这是您第一次阅读频道,请将其设置为“0”,或者如果您已经使用此脚本下载媒体,它将有一些数字,这些数字会在脚本成功执行后自动更新。不要改变它。 +- **chat_id** - 您要下载媒体的聊天/频道的 ID。你从上述步骤中得到的。 +- **last_read_message_id** - 如果这是您第一次阅读频道,请将其设置为“0”,或者如果您已经使用此脚本下载媒体,它将有一些数字,这些数字会在脚本成功执行后自动更新。不要改变它。 +- **ids_to_retry** - `保持原样。`下载器脚本使用它来跟踪所有跳过的下载,以便在下次执行脚本时可以下载它。 +- **media_types** - 要下载的媒体类型,您可以更新要下载的媒体类型,它可以是一种或任何可用类型。 +- **file_formats** - 为支持的媒体类型(“音频”、“文档”和“视频”)下载的文件类型。默认格式为“all”,下载所有文件。 +- **save_path** - 你想存储下载文件的根目录 +- **file_path_prefix** - 存储文件子文件夹,列表的顺序不定,可以随机组合 + - `chat_title` - 聊天频道或者群组标题, 如果找不到标题则为配置文件中的`chat_id` + - `media_datetime` - 资源的发布时间 + - `media_type` - 资源类型,类型查阅 `media_types` +- **upload_drive** - 您可以将文件上传到云盘 + - `enable_upload_file` - [必填]启用上传文件,默认为`false` + - `remote_dir` - [必填]你上传的地方 + - `upload_adapter` - [必填]上传文件适配器,可以为`rclone`,`aligo`。如果为`rclone`,则支持rclone所有支持上传的服务器,如果为aligo,则支持上传阿里云盘 + - `rclone_path`,如果配置`upload_adapter`为`rclone`则为必填,`rclone`的可执行目录,查阅 [如何使用rclone](https://github.com/tangyoha/telegram_media_downloader/wiki/Rclone) + - `before_upload_file_zip` - 上传前压缩文件,默认为`false` + - `after_upload_file_delete` - 上传成功后删除文件,默认为`false` +- **file_name_prefix** - 自定义文件名称,使用和 **file_path_prefix** 一样 + - `message_id` - 消息id + - `file_name` - 文件名称(可能为空) + - `caption` - 消息的标题(可能为空) +- **file_name_prefix_split** - 自定义文件名称分割符号,默认为` - ` +- **max_download_task** - 最大任务下载任务个数,默认为5个。 +- **hide_file_name** - 是否隐藏web界面文件名称,默认`false` +- **web_host** - web界面地址 +- **web_port** - web界面端口 +- **language** - 应用语言,默认为英文(`EN`),可选`ZH`(中文),`RU`,`UA` +- **web_login_secret** - 网页登录密码,如果不配置则访问网页不需要登录 +- **log_level** - 默认日志等级,请参阅 `logging._nameToLevel` +- **forward_limit** - 限制每分钟转发次数,默认为33,默认请不要修改该参数 +- **allowed_user_ids** - 允许哪些人使用机器人,默认登录账号可以使用,带@的名称请加单引号 +- **date_format** - 支持自定义配置file_path_prefix中media_datetime的格式,具体格式查看 [python-datetime](https://docs.python.org/zh-cn/3/library/time.html) +- **enable_download_txt** 启用下载txt文件,默认`false` + +## 执行 + +```sh +python3 media_downloader.py +``` + +所有下载的媒体都将存储在`save_path`根目录下。 +具体位置参考如下: + +```yaml +file_path_prefix: + - chat_title + - media_datetime + - media_type +``` + +视频下载完整目录为:`save_path`/`chat_title`/`media_datetime`/`media_type`。 +列表的顺序不定,可以随机组合。 +如果配置为空,则所有文件保存在`save_path`下。 + +## 代理 + +该项目目前支持 socks4、socks5、http 代理。要使用它,请将以下内容添加到`config.yaml`文件的底部 + +```yaml +proxy: + scheme: socks5 + hostname: 127.0.0.1 + port: 1234 + username: 你的用户名(无则删除该行) + password: 你的密码(无则删除该行) +``` + +如果您的代理不需要授权,您可以省略用户名和密码。然后代理将自动启用。 + +## 贡献 + +### 贡献指南 + +通读我们的[贡献指南](./CONTRIBUTING.md),了解我们的提交流程、编码规则等。 + +### 想帮忙? + +想要提交错误、贡献一些代码或改进文档?出色的!阅读我们的 [贡献指南](./CONTRIBUTING.md)。 + +### 行为守则 + +帮助我们保持 Telegram Media Downloader 的开放性和包容性。请阅读并遵守我们的[行为准则](./CODE_OF_CONDUCT.md)。 + + +### 赞助 + +

+Code style: black +Code style: black +

diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..1fbaabe --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% + if_no_uploads: error + if_not_found: success + if_ci_failed: error + patch: no + +comment: + require_changes: true diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..c0d7c62 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,9 @@ +black==22.6.0 +isort==5.10.1 +mock==4.0.3 +mypy==0.971 +pre-commit==2.20.0 +pylint==2.14.5 +pytest==7.2.1 +pytest-cov==3.0.0 +types-PyYAML==6.0.11 diff --git a/do_login.py b/do_login.py new file mode 100644 index 0000000..7df81ef --- /dev/null +++ b/do_login.py @@ -0,0 +1,67 @@ +""" +首次登录脚本 —— 完成 Telegram 手机号 + 验证码认证,生成 session 文件。 +只需运行一次,之后重启容器无需重新登录。 + +用法: + docker exec -it <容器名> python3 /app/do_login.py +""" + +import asyncio +import os +import sys + +import yaml +from pyrogram import Client + + +def load_config(path: str = "config.yaml") -> dict: + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} + + +async def main(): + config_path = os.path.join(os.path.abspath("."), "config.yaml") + if not os.path.exists(config_path): + print(f"错误:找不到配置文件 {config_path}") + sys.exit(1) + + cfg = load_config(config_path) + api_id = cfg.get("api_id") + api_hash = cfg.get("api_hash") + + if not api_id or not api_hash: + print("错误:config.yaml 中缺少 api_id 或 api_hash") + sys.exit(1) + + session_dir = os.path.join(os.path.abspath("."), "sessions") + os.makedirs(session_dir, exist_ok=True) + session_file = os.path.join(session_dir, "media_downloader") + + proxy_cfg = cfg.get("proxy") + proxy = None + if proxy_cfg: + proxy = { + "scheme": proxy_cfg.get("scheme", "socks5"), + "hostname": proxy_cfg.get("hostname", "127.0.0.1"), + "port": proxy_cfg.get("port", 1080), + } + + print("=== Telegram 首次登录 ===") + print(f"Session 文件将保存到:{session_file}.session") + print() + + client = Client( + session_file, + api_id=api_id, + api_hash=api_hash, + proxy=proxy, + ) + + async with client: + me = await client.get_me() + print(f"\n登录成功!账号:{me.first_name} (@{me.username})") + print("Session 文件已保存,后续启动无需重新登录。") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..e69504e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,22 @@ +version: "3.8" + +services: + telegram_media_downloader: + image: tangyoha/telegram_media_downloader:latest + build: . + + # Linux 下使用宿主机网络栈,容器自动继承宿主机的 VPN/代理 + # macOS / Windows Docker Desktop 不支持 host 模式,请注释此行并启用 ports + network_mode: host + + # macOS / Windows 用户请注释掉上面的 network_mode,并启用: + # ports: + # - "5000:5000" + + volumes: + # 配置、session、进度数据统一持久化到 ./appdata 目录 + - "./appdata:/app/appdata" + # 下载文件保存目录 + - "./downloads:/app/downloads" + + restart: unless-stopped diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..2230031 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Docker 容器入口脚本 +# 确保持久化目录和符号链接就绪,然后启动应用 +# API 凭证通过 Web 配置向导填写,无需手动配置 + +set -e + +APPDATA="/app/appdata" + +# 确保持久化子目录存在 +mkdir -p "$APPDATA/sessions" "$APPDATA/temp" "$APPDATA/log" + +# 确保 data.yaml 文件存在 +[ -f "$APPDATA/data.yaml" ] || touch "$APPDATA/data.yaml" + +# 创建符号链接,让应用从 /app/ 工作目录找到配置和数据 +# config.yaml 可能不存在——应用启动时会自动创建默认版本 +[ -L /app/config.yaml ] || ln -sf "$APPDATA/config.yaml" /app/config.yaml +[ -L /app/data.yaml ] || ln -sf "$APPDATA/data.yaml" /app/data.yaml +[ -L /app/sessions ] || ln -sf "$APPDATA/sessions" /app/sessions +[ -L /app/temp ] || ln -sf "$APPDATA/temp" /app/temp +[ -L /app/log ] || ln -sf "$APPDATA/log" /app/log + +# 若 config.yaml 已存在且设置了 WEB_PORT 环境变量,更新配置文件里的端口 +if [ -n "$WEB_PORT" ] && [ -f "$APPDATA/config.yaml" ]; then + sed -i "s/^web_port:.*/web_port: ${WEB_PORT}/" "$APPDATA/config.yaml" +fi + +# 若设置了代理环境变量,写入 config.yaml +if [ -n "$PROXY_HOSTNAME" ] && [ -f "$APPDATA/config.yaml" ]; then + python3 -c " +import yaml, os, sys +path = '$APPDATA/config.yaml' +with open(path) as f: + cfg = yaml.safe_load(f) or {} +cfg['proxy'] = { + 'scheme': os.environ.get('PROXY_SCHEME', 'socks5'), + 'hostname': os.environ.get('PROXY_HOSTNAME'), + 'port': int(os.environ.get('PROXY_PORT', 7890)) +} +with open(path, 'w') as f: + yaml.dump(cfg, f, allow_unicode=True, default_flow_style=False) +print('[entrypoint] 代理配置已写入: ' + cfg['proxy']['scheme'] + '://' + cfg['proxy']['hostname'] + ':' + str(cfg['proxy']['port'])) +" +fi + +echo "[entrypoint] 启动应用..." +exec python3 /app/media_downloader.py diff --git a/gen_filter_cache.py b/gen_filter_cache.py new file mode 100644 index 0000000..903c084 --- /dev/null +++ b/gen_filter_cache.py @@ -0,0 +1,3 @@ +from module.filter import Filter + +Filter() diff --git a/media_downloader.py b/media_downloader.py new file mode 100644 index 0000000..03261af --- /dev/null +++ b/media_downloader.py @@ -0,0 +1,840 @@ +"""Downloads media from telegram.""" +import asyncio +import logging +import os +import shutil +import time +from typing import List, Optional, Tuple, Union + +import pyrogram +from loguru import logger +from pyrogram.types import Audio, Document, Photo, Video, VideoNote, Voice +from rich.logging import RichHandler + +import module.database as db +from module.app import Application, ChatDownloadConfig, DownloadStatus, TaskNode +from module.bot import start_download_bot, stop_download_bot +from module.download_stat import ( + update_download_status, + update_task_progress, + reset_task_progress, + increment_task_stat, + get_task_progress, + is_message_skipped, + skip_message, + remove_download_entry, + clear_skipped_message, +) +from module.get_chat_history_v2 import get_chat_history_v2 +from module.language import _t +from module.pyrogram_extension import ( + HookClient, + fetch_message, + get_extension, + record_download_status, + report_bot_download_status, + set_max_concurrent_transmissions, + set_meta_data, + update_cloud_upload_stat, + upload_telegram_chat, +) +from module.web import init_web, shutdown_web +from utils.format import truncate_filename, validate_title +from utils.log import LogFilter +from utils.meta import print_meta +from utils.meta_data import MetaData + +logging.basicConfig( + level=logging.INFO, + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler()], +) + +CONFIG_NAME = "config.yaml" +DATA_FILE_NAME = "data.yaml" +APPLICATION_NAME = "media_downloader" +app = Application(CONFIG_NAME, DATA_FILE_NAME, APPLICATION_NAME) + +queue: asyncio.Queue = asyncio.Queue() +RETRY_TIME_OUT = 3 + +logging.getLogger("pyrogram.session.session").addFilter(LogFilter()) +logging.getLogger("pyrogram.client").addFilter(LogFilter()) + +logging.getLogger("pyrogram").setLevel(logging.WARNING) + + +def _check_download_finish(media_size: int, download_path: str, ui_file_name: str): + """Check download task if finish + + Parameters + ---------- + media_size: int + The size of the downloaded resource + download_path: str + Resource download hold path + ui_file_name: str + Really show file name + + """ + download_size = os.path.getsize(download_path) + if media_size == download_size: + logger.success(f"{_t('Successfully downloaded')} - {ui_file_name}") + else: + logger.warning( + f"{_t('Media downloaded with wrong size')}: " + f"{download_size}, {_t('actual')}: " + f"{media_size}, {_t('file name')}: {ui_file_name}" + ) + os.remove(download_path) + raise pyrogram.errors.exceptions.bad_request_400.BadRequest() + + +def _move_to_download_path(temp_download_path: str, download_path: str): + """Move file to download path + + Parameters + ---------- + temp_download_path: str + Temporary download path + + download_path: str + Download path + + """ + + directory, _ = os.path.split(download_path) + os.makedirs(directory, exist_ok=True) + shutil.move(temp_download_path, download_path) + + +def _check_timeout(retry: int, _: int): + """Check if message download timeout, then add message id into failed_ids + + Parameters + ---------- + retry: int + Retry download message times + + message_id: int + Try to download message 's id + + """ + if retry == 2: + return True + return False + + +def _can_download(_type: str, file_formats: dict, file_format: Optional[str]) -> bool: + """ + Check if the given file format can be downloaded. + + Parameters + ---------- + _type: str + Type of media object. + file_formats: dict + Dictionary containing the list of file_formats + to be downloaded for `audio`, `document` & `video` + media types + file_format: str + Format of the current file to be downloaded. + + Returns + ------- + bool + True if the file format can be downloaded else False. + """ + if _type in ["audio", "document", "video"]: + allowed_formats: list = file_formats[_type] + if not file_format in allowed_formats and allowed_formats[0] != "all": + return False + return True + + +def _is_exist(file_path: str) -> bool: + """ + Check if a file exists and it is not a directory. + + Parameters + ---------- + file_path: str + Absolute path of the file to be checked. + + Returns + ------- + bool + True if the file exists else False. + """ + return not os.path.isdir(file_path) and os.path.exists(file_path) + + +# pylint: disable = R0912 + + +async def _get_media_meta( + chat_id: Union[int, str], + message: pyrogram.types.Message, + media_obj: Union[Audio, Document, Photo, Video, VideoNote, Voice], + _type: str, +) -> Tuple[str, str, Optional[str]]: + """Extract file name and file id from media object. + + Parameters + ---------- + media_obj: Union[Audio, Document, Photo, Video, VideoNote, Voice] + Media object to be extracted. + _type: str + Type of media object. + + Returns + ------- + Tuple[str, str, Optional[str]] + file_name, file_format + """ + if _type in ["audio", "document", "video"]: + # pylint: disable = C0301 + file_format: Optional[str] = media_obj.mime_type.split("/")[-1] # type: ignore + else: + file_format = None + + file_name = None + temp_file_name = None + dirname = validate_title(f"{chat_id}") + if message.chat and message.chat.title: + dirname = validate_title(f"{message.chat.title}") + + if message.date: + datetime_dir_name = message.date.strftime(app.date_format) + else: + datetime_dir_name = "0" + + if _type in ["voice", "video_note"]: + # pylint: disable = C0209 + file_format = media_obj.mime_type.split("/")[-1] # type: ignore + file_save_path = app.get_file_save_path(_type, dirname, datetime_dir_name) + file_name = "{} - {}_{}.{}".format( + message.id, + _type, + media_obj.date.isoformat(), # type: ignore + file_format, + ) + file_name = validate_title(file_name) + temp_file_name = os.path.join(app.temp_save_path, dirname, file_name) + + file_name = os.path.join(file_save_path, file_name) + else: + file_name = getattr(media_obj, "file_name", None) + caption = getattr(message, "caption", None) + + file_name_suffix = ".unknown" + if not file_name: + file_name_suffix = get_extension( + media_obj.file_id, getattr(media_obj, "mime_type", "") + ) + else: + # file_name = file_name.split(".")[0] + _, file_name_without_suffix = os.path.split(os.path.normpath(file_name)) + file_name, file_name_suffix = os.path.splitext(file_name_without_suffix) + if not file_name_suffix: + file_name_suffix = get_extension( + media_obj.file_id, getattr(media_obj, "mime_type", "") + ) + + if caption: + caption = validate_title(caption) + app.set_caption_name(chat_id, message.media_group_id, caption) + app.set_caption_entities( + chat_id, message.media_group_id, message.caption_entities + ) + else: + caption = app.get_caption_name(chat_id, message.media_group_id) + + if not file_name and message.photo: + file_name = f"{message.photo.file_unique_id}" + + gen_file_name = ( + app.get_file_name(message.id, file_name, caption) + file_name_suffix + ) + + file_save_path = app.get_file_save_path(_type, dirname, datetime_dir_name) + + temp_file_name = os.path.join(app.temp_save_path, dirname, gen_file_name) + + file_name = os.path.join(file_save_path, gen_file_name) + return truncate_filename(file_name), truncate_filename(temp_file_name), file_format + + +async def add_download_task( + message: pyrogram.types.Message, + node: TaskNode, +): + """Add Download task""" + if message.empty: + return False + node.download_status[message.id] = DownloadStatus.Downloading + await queue.put((message, node)) + node.total_task += 1 + return True + + +async def save_msg_to_file( + app, chat_id: Union[int, str], message: pyrogram.types.Message +): + """Write message text into file""" + dirname = validate_title( + message.chat.title if message.chat and message.chat.title else str(chat_id) + ) + datetime_dir_name = message.date.strftime(app.date_format) if message.date else "0" + + file_save_path = app.get_file_save_path("msg", dirname, datetime_dir_name) + file_name = os.path.join( + app.temp_save_path, + file_save_path, + f"{app.get_file_name(message.id, None, None)}.txt", + ) + + os.makedirs(os.path.dirname(file_name), exist_ok=True) + + if _is_exist(file_name): + return DownloadStatus.SkipDownload, None + + with open(file_name, "w", encoding="utf-8") as f: + f.write(message.text or "") + + return DownloadStatus.SuccessDownload, file_name + + +async def download_task( + client: pyrogram.Client, message: pyrogram.types.Message, node: TaskNode +): + """Download and Forward media""" + + # Track download start + increment_task_stat("downloading_files") + + download_status, file_name = await download_media( + client, message, app.media_types, app.file_formats, node + ) + + # Track download completion + increment_task_stat("downloading_files", -1) + if download_status == DownloadStatus.SuccessDownload: + increment_task_stat("completed_files") + elif download_status == DownloadStatus.FailedDownload: + increment_task_stat("failed_files") + + if app.enable_download_txt and message.text and not message.media: + download_status, file_name = await save_msg_to_file(app, node.chat_id, message) + + if not node.bot: + app.set_download_id(node, message.id, download_status) + + node.download_status[message.id] = download_status + + file_size = os.path.getsize(file_name) if file_name else 0 + + await upload_telegram_chat( + client, + node.upload_user if node.upload_user else client, + app, + node, + message, + download_status, + file_name, + ) + + # rclone upload + if ( + not node.upload_telegram_chat_id + and download_status is DownloadStatus.SuccessDownload + ): + ui_file_name = file_name + if app.hide_file_name: + ui_file_name = f"****{os.path.splitext(file_name)[-1]}" + if await app.upload_file( + file_name, update_cloud_upload_stat, (node, message.id, ui_file_name) + ): + node.upload_success_count += 1 + + await report_bot_download_status( + node.bot, + node, + download_status, + file_size, + ) + + +# pylint: disable = R0915,R0914 + + +@record_download_status +async def download_media( + client: pyrogram.client.Client, + message: pyrogram.types.Message, + media_types: List[str], + file_formats: dict, + node: TaskNode, +): + """ + Download media from Telegram. + + Each of the files to download are retried 3 times with a + delay of 5 seconds each. + + Parameters + ---------- + client: pyrogram.client.Client + Client to interact with Telegram APIs. + message: pyrogram.types.Message + Message object retrieved from telegram. + media_types: list + List of strings of media types to be downloaded. + Ex : `["audio", "photo"]` + Supported formats: + * audio + * document + * photo + * video + * voice + file_formats: dict + Dictionary containing the list of file_formats + to be downloaded for `audio`, `document` & `video` + media types. + + Returns + ------- + int + Current message id. + """ + + # pylint: disable = R0912 + + file_name: str = "" + ui_file_name: str = "" + task_start_time: float = time.time() + media_size = 0 + _media = None + message = await fetch_message(client, message) + try: + for _type in media_types: + _media = getattr(message, _type, None) + if _media is None: + continue + file_name, temp_file_name, file_format = await _get_media_meta( + node.chat_id, message, _media, _type + ) + media_size = getattr(_media, "file_size", 0) + + ui_file_name = file_name + if app.hide_file_name: + ui_file_name = f"****{os.path.splitext(file_name)[-1]}" + + if _can_download(_type, file_formats, file_format): + if _is_exist(file_name): + file_size = os.path.getsize(file_name) + if file_size or file_size == media_size: + logger.info( + f"id={message.id} {ui_file_name} " + f"{_t('already download,download skipped')}.\n" + ) + # Update skip counter + increment_task_stat("skipped_files") + increment_task_stat("checked_messages") + return DownloadStatus.SkipDownload, None + else: + _should_skip, _reason = db.should_skip(str(node.chat_id), message.id) + if _should_skip: + logger.info( + f"id={message.id} {ui_file_name} {_reason},跳过。\n" + ) + increment_task_stat("skipped_files") + increment_task_stat("checked_messages") + return DownloadStatus.SkipDownload, None + else: + increment_task_stat("checked_messages") + return DownloadStatus.SkipDownload, None + + break + except Exception as e: + logger.error( + f"Message[{message.id}]: " + f"{_t('could not be downloaded due to following exception')}:\n[{e}].", + exc_info=True, + ) + return DownloadStatus.FailedDownload, None + if _media is None: + return DownloadStatus.SkipDownload, None + + message_id = message.id + + for retry in range(3): + if is_message_skipped(str(node.chat_id), message_id): + clear_skipped_message(str(node.chat_id), message_id) + remove_download_entry(str(node.chat_id), message_id) + return DownloadStatus.SkipDownload, None + try: + temp_download_path = await client.download_media( + message, + file_name=temp_file_name, + progress=update_download_status, + progress_args=( + message_id, + ui_file_name, + task_start_time, + node, + client, + ), + ) + + if temp_download_path and isinstance(temp_download_path, str): + _check_download_finish(media_size, temp_download_path, ui_file_name) + await asyncio.sleep(0.5) + _move_to_download_path(temp_download_path, file_name) + chat_title = "" + if message.chat and message.chat.title: + chat_title = message.chat.title + db.record_download( + chat_id=str(node.chat_id), + chat_title=chat_title, + message_id=message.id, + file_name=os.path.basename(file_name), + file_path=file_name, + file_size=media_size, + media_type=_type, + status="success", + ) + # TODO: if not exist file size or media + return DownloadStatus.SuccessDownload, file_name + except pyrogram.errors.exceptions.bad_request_400.BadRequest: + logger.warning( + f"Message[{message.id}]: {_t('file reference expired, refetching')}..." + ) + await asyncio.sleep(RETRY_TIME_OUT) + message = await fetch_message(client, message) + if _check_timeout(retry, message.id): + # pylint: disable = C0301 + logger.error( + f"Message[{message.id}]: " + f"{_t('file reference expired for 3 retries, download skipped.')}" + ) + except pyrogram.errors.exceptions.flood_420.FloodWait as wait_err: + await asyncio.sleep(wait_err.value) + logger.warning("Message[{}]: FlowWait {}", message.id, wait_err.value) + _check_timeout(retry, message.id) + except TypeError: + # pylint: disable = C0301 + logger.warning( + f"{_t('Timeout Error occurred when downloading Message')}[{message.id}], " + f"{_t('retrying after')} {RETRY_TIME_OUT} {_t('seconds')}" + ) + await asyncio.sleep(RETRY_TIME_OUT) + if _check_timeout(retry, message.id): + logger.error( + f"Message[{message.id}]: {_t('Timing out after 3 reties, download skipped.')}" + ) + except Exception as e: + # pylint: disable = C0301 + logger.error( + f"Message[{message.id}]: " + f"{_t('could not be downloaded due to following exception')}:\n[{e}].", + exc_info=True, + ) + break + + return DownloadStatus.FailedDownload, None + + +def _load_config(): + """Load config""" + app.load_config() + + +def _check_config() -> bool: + """Check config""" + print_meta(logger) + try: + _load_config() + logger.add( + os.path.join(app.log_file_path, "tdl.log"), + rotation="10 MB", + retention="10 days", + level=app.log_level, + ) + except Exception as e: + logger.exception(f"load config error: {e}") + return False + + return True + + +async def worker(client: pyrogram.client.Client): + """Work for download task""" + while app.is_running: + try: + item = await queue.get() + message = item[0] + node: TaskNode = item[1] + + if node.is_stop_transmission: + continue + + if is_message_skipped(str(node.chat_id), message.id): + skip_message(str(node.chat_id), message.id) + continue + + if node.client: + await download_task(node.client, message, node) + else: + await download_task(client, message, node) + except Exception as e: + logger.exception(f"{e}") + + +async def download_chat_task( + client: pyrogram.Client, + chat_download_config: ChatDownloadConfig, + node: TaskNode, +): + """Download all task""" + # Reset and update task progress + reset_task_progress() + + # Try to get chat title + try: + chat = await client.get_chat(node.chat_id) + chat_title = chat.title or chat.first_name or str(node.chat_id) + except Exception: + chat_title = str(node.chat_id) + + update_task_progress( + current_chat=str(node.chat_id), + current_chat_title=chat_title, + is_checking=True + ) + + # 改动点 A:尝试读预扫描缓存;命中则 banner 立刻显示 0 / N + filter_key = db.build_filter_key( + chat_download_config.download_filter, + app.media_types, + app.file_formats, + ) + cached_total = db.get_scan_cache(str(node.chat_id), filter_key) + if cached_total is not None: + update_task_progress(estimated_total=cached_total) + + messages_iter = get_chat_history_v2( + client, + node.chat_id, + limit=node.limit, + max_id=node.end_offset_id, + offset_id=chat_download_config.last_read_message_id, + reverse=True, + ) + + chat_download_config.node = node + + if chat_download_config.ids_to_retry: + logger.info(f"{_t('Downloading files failed during last run')}...") + skipped_messages: list = await client.get_messages( # type: ignore + chat_id=node.chat_id, message_ids=chat_download_config.ids_to_retry + ) + + for message in skipped_messages: + await add_download_task(message, node) + + async for message in messages_iter: # type: ignore + # Update checking progress for each message + increment_task_stat("checked_messages") + + meta_data = MetaData() + + caption = message.caption + if caption: + caption = validate_title(caption) + app.set_caption_name(node.chat_id, message.media_group_id, caption) + app.set_caption_entities( + node.chat_id, message.media_group_id, message.caption_entities + ) + else: + caption = app.get_caption_name(node.chat_id, message.media_group_id) + set_meta_data(meta_data, message, caption) + + if app.need_skip_message(chat_download_config, message.id): + continue + + if app.exec_filter(chat_download_config, meta_data): + # 改动点 B:通过 filter 的消息计数,作为 X/N 的分母实时递增 + increment_task_stat("qualified_files") + await add_download_task(message, node) + else: + node.download_status[message.id] = DownloadStatus.SkipDownload + increment_task_stat("skipped_files") + if message.media_group_id: + await upload_telegram_chat( + client, + node.upload_user, + app, + node, + message, + DownloadStatus.SkipDownload, + ) + + chat_download_config.need_check = True + chat_download_config.total_task = node.total_task + node.is_running = True + + # 改动点 C:遍历正常完成,把实际 qualified_files 写入缓存并覆盖 estimated_total + # 仅在正常完成时写入(异常/中断时不执行,避免脏缓存) + actual_total = get_task_progress().get("qualified_files", 0) + db.save_scan_cache(str(node.chat_id), filter_key, actual_total) + update_task_progress(estimated_total=actual_total, is_checking=False) + + +async def download_all_chat(client: pyrogram.Client): + """Download All chat""" + # Use list() to avoid "dictionary changed size during iteration" error + for key, value in list(app.chat_download_config.items()): + value.node = TaskNode(chat_id=key) + try: + await download_chat_task(client, value, value.node) + except Exception as e: + logger.warning(f"Download {key} error: {e}") + finally: + value.need_check = True + + +async def run_until_all_task_finish(): + """Normal download""" + while True: + finish: bool = True + for _, value in app.chat_download_config.items(): + if not value.need_check or value.total_task != value.finish_task: + finish = False + + if app.restart_program: + break + + await asyncio.sleep(1) + + +def _exec_loop(): + """Exec loop""" + + app.loop.run_until_complete(run_until_all_task_finish()) + + +async def start_server(client: pyrogram.Client): + """ + Start the server using the provided client. + """ + await client.start() + + +async def stop_server(client: pyrogram.Client): + """ + Stop the server using the provided client. + """ + await client.stop() + + +def main(): + """Main function of the downloader.""" + tasks = [] + + # 未填写 API 凭证时以"Web 配置模式"启动,跳过 Telegram 客户端初始化 + if app.is_configured(): + client = HookClient( + "media_downloader", + api_id=app.api_id, + api_hash=app.api_hash, + proxy=app.proxy, + workdir=app.session_file_path, + start_timeout=app.start_timeout, + ) + else: + client = None + logger.info( + "未检测到 API 凭证,以 Web 配置模式启动。" + f"请访问 http://0.0.0.0:{app.web_port} 完成初始设置。" + ) + + try: + app.pre_run() + db.init_db(os.path.join(os.path.abspath("."), "appdata", "downloads.db")) + init_web(app, client, add_download_task, download_chat_task) + + if client is not None: + set_max_concurrent_transmissions(client, app.max_concurrent_transmissions) + app.loop.run_until_complete(start_server(client)) + logger.success(_t("Successfully started (Press Ctrl+C to stop)")) + + app.loop.create_task(download_all_chat(client)) + for _ in range(app.max_download_task): + task = app.loop.create_task(worker(client)) + tasks.append(task) + + if app.bot_token: + app.loop.run_until_complete( + start_download_bot(app, client, add_download_task, download_chat_task) + ) + _exec_loop() + else: + # Web 配置模式:保持进程运行,等待用户完成 Web 向导配置后触发重启 + async def _wait_for_restart(): + while not app.restart_program: + await asyncio.sleep(1) + app.loop.run_until_complete(_wait_for_restart()) + except KeyboardInterrupt: + logger.info(_t("KeyboardInterrupt")) + except Exception as e: + logger.exception("{}", e) + finally: + app.is_running = False + if client is not None: + if app.bot_token: + app.loop.run_until_complete(stop_download_bot()) + try: + app.loop.run_until_complete(stop_server(client)) + except Exception: + pass + for task in tasks: + task.cancel() + logger.info(_t("Stopped!")) + # check_for_updates(app.proxy) + # Web 配置模式下重启时,不用内存中的空凭证覆盖向导刚写入的配置文件 + if not (client is None and app.restart_program): + logger.info(f"{_t('update config')}......") + app.update_config() + logger.success( + f"{_t('Updated last read message_id to config file')}," + f"{_t('total download')} {app.total_download_task}, " + f"{_t('total upload file')} " + f"{app.cloud_drive_config.total_upload_success_file_count}" + ) + + # Return whether restart is needed + return app.restart_program + + +def run_with_restart(): + """Run main with auto-restart support using os.execv for clean restart""" + import sys + + should_restart = main() + + if should_restart: + logger.info("🔄 正在重启程序以应用新配置...") + # Shutdown web server to release the port + shutdown_web() + # Wait a moment for the port to be released + time.sleep(1) + # Use os.execv to completely restart the process + python = sys.executable + os.execv(python, [python] + sys.argv) + + +if __name__ == "__main__": + if _check_config(): + run_with_restart() diff --git a/media_downloader.spec b/media_downloader.spec new file mode 100644 index 0000000..3dc6f1b --- /dev/null +++ b/media_downloader.spec @@ -0,0 +1,45 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['media_downloader.py'], + pathex=[], + binaries=[], + datas=[('./module/templates','./module/templates'),('./module/static/','./module/static'), ('./module/parsetab.py','./module/'),('./module/parser.out','./module/'),('./config.yaml','./'),('./data.yaml','./')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='tdl', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + contents_directory='.', +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='tdl', +) diff --git a/module/__init__.py b/module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module/app.py b/module/app.py new file mode 100644 index 0000000..bd05ca9 --- /dev/null +++ b/module/app.py @@ -0,0 +1,1033 @@ +"""Application module""" + +import asyncio +import os +import time +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from typing import Callable, List, Optional, Union + +from loguru import logger +from ruamel import yaml + +from module.cloud_drive import CloudDrive, CloudDriveConfig +from module.filter import Filter +from module.language import Language, set_language +from utils.format import replace_date_time, validate_title +from utils.meta_data import MetaData + +_yaml = yaml.YAML() +# pylint: disable = R0902 + + +class DownloadStatus(Enum): + """Download status""" + + SkipDownload = 1 + SuccessDownload = 2 + FailedDownload = 3 + Downloading = 4 + + +class ForwardStatus(Enum): + """Forward status""" + + SkipForward = 1 + SuccessForward = 2 + FailedForward = 3 + Forwarding = 4 + StopForward = 5 + CacheForward = 6 + + +class UploadStatus(Enum): + """Upload status""" + + SkipUpload = 1 + SuccessUpload = 2 + FailedUpload = 3 + Uploading = 4 + + +class TaskType(Enum): + """Task Type""" + + Download = 1 + Forward = 2 + ListenForward = 3 + + +class QueryHandler(Enum): + """Query handler""" + + StopDownload = 1 + StopForward = 2 + StopListenForward = 3 + + +@dataclass +class UploadProgressStat: + """Upload task""" + + file_name: str + total_size: int + upload_size: int + start_time: float + last_stat_time: float + upload_speed: float + + +@dataclass +class CloudDriveUploadStat: + """Cloud drive upload task""" + + file_name: str + transferred: str + total: str + percentage: str + speed: str + eta: str + + +class QueryHandlerStr: + """Query handler""" + + _strMap = { + QueryHandler.StopDownload.value: "stop_download", + QueryHandler.StopForward.value: "stop_forward", + QueryHandler.StopListenForward.value: "stop_listen_forward", + } + + @staticmethod + def get_str(value): + """ + Get the string value associated with the given value. + + Parameters: + value (any): The value for which to retrieve the string value. + + Returns: + str: The string value associated with the given value. + """ + return QueryHandlerStr._strMap[value] + + +class TaskNode: + """Task node""" + + # pylint: disable = R0913 + def __init__( + self, + chat_id: Union[int, str], + from_user_id: Union[int, str] = None, + reply_message_id: int = 0, + replay_message: str = None, + upload_telegram_chat_id: Union[int, str] = None, + has_protected_content: bool = False, + download_filter: str = None, + limit: int = 0, + start_offset_id: int = 0, + end_offset_id: int = 0, + bot=None, + task_type: TaskType = TaskType.Download, + task_id: int = 0, + topic_id: int = 0, + ): + self.chat_id = chat_id + self.from_user_id = from_user_id + self.upload_telegram_chat_id = upload_telegram_chat_id + self.reply_message_id = reply_message_id + self.reply_message = replay_message + self.has_protected_content = has_protected_content + self.download_filter = download_filter + self.limit = limit + self.start_offset_id = start_offset_id + self.end_offset_id = end_offset_id + self.bot = bot + self.task_id = task_id + self.task_type = task_type + self.total_task = 0 + self.total_download_task = 0 + self.failed_download_task = 0 + self.success_download_task = 0 + self.skip_download_task = 0 + self.last_reply_time = time.time() + self.last_edit_msg: str = "" + self.total_download_byte = 0 + self.forward_msg_detail_str: str = "" + self.upload_user = None + self.total_forward_task: int = 0 + self.success_forward_task: int = 0 + self.failed_forward_task: int = 0 + self.skip_forward_task: int = 0 + self.is_running: bool = False + self.client = None + self.upload_success_count: int = 0 + self.is_stop_transmission = False + self.media_group_ids: dict = {} + self.download_status: dict = {} + self.upload_status: dict = {} + self.upload_stat_dict: dict = {} + self.topic_id = topic_id + self.reply_to_message = None + self.cloud_drive_upload_stat_dict: dict = {} + + def skip_msg_id(self, msg_id: int): + """Skip if message id out of range""" + if self.start_offset_id and msg_id < self.start_offset_id: + return True + + if self.end_offset_id and msg_id > self.end_offset_id: + return True + + return False + + def is_finish(self): + """If is finish""" + return self.is_stop_transmission or ( + self.is_running + and self.task_type != TaskType.ListenForward + and self.total_task == self.total_download_task + ) + + def stop_transmission(self): + """Stop task""" + self.is_stop_transmission = True + + def stat(self, status: DownloadStatus): + """ + Updates the download status of the task. + + Args: + status (DownloadStatus): The status of the download task. + + Returns: + None + """ + self.total_download_task += 1 + if status is DownloadStatus.SuccessDownload: + self.success_download_task += 1 + elif status is DownloadStatus.SkipDownload: + self.skip_download_task += 1 + else: + self.failed_download_task += 1 + + def stat_forward(self, status: ForwardStatus, count: int = 1): + """Stat upload""" + self.total_forward_task += count + if status is ForwardStatus.SuccessForward: + self.success_forward_task += count + elif status is ForwardStatus.SkipForward: + self.skip_forward_task += count + else: + self.failed_forward_task += count + + def can_reply(self): + """ + Checks if the bot can reply to a message + based on the time elapsed since the last reply. + + Returns: + True if the time elapsed since + the last reply is greater than 1 second, False otherwise. + """ + cur_time = time.time() + if cur_time - self.last_reply_time > 1.0: + self.last_reply_time = cur_time + return True + + return False + + +class LimitCall: + """Limit call""" + + def __init__( + self, + max_limit_call_times: int = 0, + limit_call_times: int = 0, + last_call_time: float = 0, + ): + """ + Initializes the object with the given parameters. + + Args: + max_limit_call_times (int): The maximum limit of call times allowed. + limit_call_times (int): The current limit of call times. + last_call_time (int): The time of the last call. + + Returns: + None + """ + self.max_limit_call_times = max_limit_call_times + self.limit_call_times = limit_call_times + self.last_call_time = last_call_time + + async def wait(self, node: TaskNode): + """ + Wait for a certain period of time before continuing execution. + + This function does not take any parameters. + + This function does not return anything. + """ + while True: + now = time.time() + time_span = now - self.last_call_time + if node.is_stop_transmission: + break + + if time_span > 60: + self.limit_call_times = 0 + self.last_call_time = now + + if self.limit_call_times + 1 <= self.max_limit_call_times: + self.limit_call_times += 1 + break + + # logger.debug("Waiting for 10 seconds...") + await asyncio.sleep(1) + + +class ChatDownloadConfig: + """Chat Message Download Status""" + + def __init__(self): + self.ids_to_retry_dict: dict = {} + + # need storage + self.download_filter: str = None + self.ids_to_retry: list = [] + self.last_read_message_id = 0 + self.total_task: int = 0 + self.finish_task: int = 0 + self.need_check: bool = False + self.upload_telegram_chat_id: Union[int, str] = None + self.node: TaskNode = TaskNode(0) + + +def get_config(config, key, default=None, val_type=str, verbose=True): + """ + Retrieves a configuration value from the given `config` dictionary + based on the specified `key`. + + Args: + config (dict): A dictionary containing the configuration values. + key (str): The key of the configuration value to retrieve. + default (Any, optional): The default value to be returned + if the `key` is not found. + val_type (type, optional): The data type of the configuration value. + verbose (bool, optional): A flag indicating whether to print + a warning message if the `key` is not found. + + Returns: + The configuration value associated with the specified `key`, + converted to the specified `type`. If the `key` is not found, + the `default` value is returned. + """ + val = config.get(key, default) + if isinstance(val, val_type): + return val + + if verbose: + logger.warning(f"{key} is not {val_type.__name__}") + + return default + + +class Application: + """Application load config and update config.""" + + def __init__( + self, + config_file: str, + app_data_file: str, + application_name: str = "UndefineApp", + ): + """ + Init and update telegram media downloader config + + Parameters + ---------- + config_file: str + Config file name + + app_data_file: str + App data file + + application_name: str + Application Name + + """ + self.config_file: str = config_file + self.app_data_file: str = app_data_file + self.application_name: str = application_name + self.download_filter = Filter() + self.is_running = True + + self.total_download_task = 0 + + self.chat_download_config: dict = {} + + self.save_path = os.path.join(os.path.abspath("."), "downloads") + self.temp_save_path = os.path.join(os.path.abspath("."), "temp") + self.api_id: str = "" + self.api_hash: str = "" + self.bot_token: str = "" + self._chat_id: str = "" + self.media_types: List[str] = [] + self.file_formats: dict = {} + self.proxy: dict = {} + self.restart_program = False + self.config: dict = {} + self.app_data: dict = {} + self.file_path_prefix: List[str] = ["chat_title", "media_datetime"] + self.file_name_prefix: List[str] = ["message_id", "file_name"] + self.file_name_prefix_split: str = " - " + self.log_file_path = os.path.join(os.path.abspath("."), "log") + self.session_file_path = os.path.join(os.path.abspath("."), "sessions") + self.cloud_drive_config = CloudDriveConfig() + self.hide_file_name = False + self.caption_name_dict: dict = {} + self.caption_entities_dict: dict = {} + self.max_concurrent_transmissions: int = 1 + self.web_host: str = "0.0.0.0" + self.web_port: int = 5000 + self.max_download_task: int = 5 + self.language = Language.EN + self.after_upload_telegram_delete: bool = True + self.web_login_secret: str = "" + self.debug_web: bool = False + self.log_level: str = "INFO" + self.start_timeout: int = 60 + self.allowed_user_ids: yaml.comments.CommentedSeq = yaml.comments.CommentedSeq( + [] + ) + self.date_format: str = "%Y_%m" + self.drop_no_audio_video: bool = False + self.enable_download_txt: bool = False + + self.forward_limit_call = LimitCall(max_limit_call_times=33) + + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + self.executor = ThreadPoolExecutor( + min(32, (os.cpu_count() or 0) + 4), thread_name_prefix="multi_task" + ) + + # pylint: disable = R0915 + def assign_config(self, _config: dict) -> bool: + """assign config from str. + + Parameters + ---------- + _config: dict + application config dict + + Returns + ------- + bool + """ + # pylint: disable = R0912 + # TODO: judge the storage if enough,and provide more path + if _config.get("save_path") is not None: + self.save_path = _config["save_path"] + + self.api_id = _config.get("api_id", 0) + self.api_hash = _config.get("api_hash", "") + self.bot_token = _config.get("bot_token", "") + + self.media_types = _config.get( + "media_types", ["audio", "photo", "video", "document", "voice", "video_note"] + ) + self.file_formats = _config.get( + "file_formats", {"audio": ["all"], "document": ["all"], "video": ["all"]} + ) + + self.hide_file_name = _config.get("hide_file_name", False) + + # option + if _config.get("proxy"): + self.proxy = _config["proxy"] + if _config.get("restart_program"): + self.restart_program = _config["restart_program"] + if _config.get("file_path_prefix"): + self.file_path_prefix = _config["file_path_prefix"] + if _config.get("file_name_prefix"): + self.file_name_prefix = _config["file_name_prefix"] + + if _config.get("upload_drive"): + upload_drive_config = _config["upload_drive"] + if upload_drive_config.get("enable_upload_file"): + self.cloud_drive_config.enable_upload_file = upload_drive_config[ + "enable_upload_file" + ] + + if upload_drive_config.get("rclone_path"): + self.cloud_drive_config.rclone_path = upload_drive_config["rclone_path"] + + if upload_drive_config.get("remote_dir"): + self.cloud_drive_config.remote_dir = upload_drive_config["remote_dir"] + + if upload_drive_config.get("before_upload_file_zip"): + self.cloud_drive_config.before_upload_file_zip = upload_drive_config[ + "before_upload_file_zip" + ] + + if upload_drive_config.get("after_upload_file_delete"): + self.cloud_drive_config.after_upload_file_delete = upload_drive_config[ + "after_upload_file_delete" + ] + + if upload_drive_config.get("upload_adapter"): + self.cloud_drive_config.upload_adapter = upload_drive_config[ + "upload_adapter" + ] + + self.file_name_prefix_split = _config.get( + "file_name_prefix_split", self.file_name_prefix_split + ) + self.web_host = _config.get("web_host", self.web_host) + self.web_port = _config.get("web_port", self.web_port) + + # TODO: add check if expression exist syntax error + + self.max_download_task = _config.get( + "max_download_task", self.max_download_task + ) + + self.max_concurrent_transmissions = self.max_download_task * 5 + + self.max_concurrent_transmissions = _config.get( + "max_concurrent_transmissions", self.max_concurrent_transmissions + ) + + language = _config.get("language", "EN") + + try: + self.language = Language[language.upper()] + except KeyError: + pass + + self.after_upload_telegram_delete = _config.get( + "after_upload_telegram_delete", self.after_upload_telegram_delete + ) + + self.web_login_secret = str( + _config.get("web_login_secret", self.web_login_secret) + ) + self.debug_web = _config.get("debug_web", self.debug_web) + self.log_level = _config.get("log_level", self.log_level) + + self.start_timeout = get_config( + _config, "start_timeout", self.start_timeout, int + ) + + self.allowed_user_ids = get_config( + _config, + "allowed_user_ids", + self.allowed_user_ids, + yaml.comments.CommentedSeq, + ) + + self.date_format = get_config( + _config, + "date_format", + self.date_format, + str, + ) + + self.drop_no_audio_video = get_config( + _config, "drop_no_audio_video", self.drop_no_audio_video, bool + ) + + self.enable_download_txt = get_config( + _config, "enable_download_txt", self.enable_download_txt, bool + ) + + try: + date = datetime(2023, 10, 31) + date.strftime(self.date_format) + except Exception as e: + logger.warning(f"config date format error: {e}") + self.date_format = "%Y_%m" + + forward_limit = _config.get("forward_limit", None) + if forward_limit: + try: + forward_limit = int(forward_limit) + self.forward_limit_call.max_limit_call_times = forward_limit + except ValueError: + pass + + if _config.get("chat"): + chat = _config["chat"] + for item in chat: + if "chat_id" in item: + self.chat_download_config[item["chat_id"]] = ChatDownloadConfig() + self.chat_download_config[ + item["chat_id"] + ].last_read_message_id = item.get("last_read_message_id", 0) + self.chat_download_config[ + item["chat_id"] + ].download_filter = item.get("download_filter", "") + self.chat_download_config[ + item["chat_id"] + ].upload_telegram_chat_id = item.get( + "upload_telegram_chat_id", None + ) + elif _config.get("chat_id"): + # Compatible with lower versions + self._chat_id = _config["chat_id"] + + self.chat_download_config[self._chat_id] = ChatDownloadConfig() + + if _config.get("ids_to_retry"): + self.chat_download_config[self._chat_id].ids_to_retry = _config[ + "ids_to_retry" + ] + for it in self.chat_download_config[self._chat_id].ids_to_retry: + self.chat_download_config[self._chat_id].ids_to_retry_dict[ + it + ] = True + + self.chat_download_config[self._chat_id].last_read_message_id = _config[ + "last_read_message_id" + ] + download_filter_dict = _config.get("download_filter", None) + + self.config["chat"] = [ + { + "chat_id": self._chat_id, + "last_read_message_id": self.chat_download_config[ + self._chat_id + ].last_read_message_id, + } + ] + + if download_filter_dict and self._chat_id in download_filter_dict: + self.chat_download_config[ + self._chat_id + ].download_filter = download_filter_dict[self._chat_id] + self.config["chat"][0]["download_filter"] = download_filter_dict[ + self._chat_id + ] + + # pylint: disable = R1733 + for key, value in self.chat_download_config.items(): + self.chat_download_config[key].download_filter = replace_date_time( + value.download_filter + ) + + return True + + def assign_app_data(self, app_data: dict) -> bool: + """Assign config from str. + + Parameters + ---------- + app_data: dict + application data dict + + Returns + ------- + bool + """ + if app_data.get("ids_to_retry"): + if self._chat_id: + self.chat_download_config[self._chat_id].ids_to_retry = app_data[ + "ids_to_retry" + ] + for it in self.chat_download_config[self._chat_id].ids_to_retry: + self.chat_download_config[self._chat_id].ids_to_retry_dict[ + it + ] = True + self.app_data.pop("ids_to_retry") + else: + if app_data.get("chat"): + chats = app_data["chat"] + for chat in chats: + if ( + "chat_id" in chat + and chat["chat_id"] in self.chat_download_config + ): + chat_id = chat["chat_id"] + self.chat_download_config[chat_id].ids_to_retry = chat.get( + "ids_to_retry", [] + ) + for it in self.chat_download_config[chat_id].ids_to_retry: + self.chat_download_config[chat_id].ids_to_retry_dict[ + it + ] = True + return True + + async def upload_file( + self, + local_file_path: str, + progress_callback: Callable = None, + progress_args: tuple = (), + ) -> bool: + """Upload file""" + + if not self.cloud_drive_config.enable_upload_file: + return False + + ret: bool = False + if self.cloud_drive_config.upload_adapter == "rclone": + ret = await CloudDrive.rclone_upload_file( + self.cloud_drive_config, + self.save_path, + local_file_path, + progress_callback, + progress_args, + ) + elif self.cloud_drive_config.upload_adapter == "aligo": + ret = await self.loop.run_in_executor( + self.executor, + CloudDrive.aligo_upload_file( + self.cloud_drive_config, self.save_path, local_file_path + ), + ) + + return ret + + def get_file_save_path( + self, media_type: str, chat_title: str, media_datetime: str + ) -> str: + """Get file save path prefix. + + Parameters + ---------- + media_type: str + see config.yaml media_types + + chat_title: str + see channel or group title + + media_datetime: str + media datetime + + Returns + ------- + str + file save path prefix + """ + + res: str = self.save_path + for prefix in self.file_path_prefix: + if prefix == "chat_title": + res = os.path.join(res, chat_title) + elif prefix == "media_datetime": + res = os.path.join(res, media_datetime) + elif prefix == "media_type": + res = os.path.join(res, media_type) + return res + + def get_file_name( + self, message_id: int, file_name: Optional[str], caption: Optional[str] + ) -> str: + """Get file save path prefix. + + Parameters + ---------- + message_id: int + Message id + + file_name: Optional[str] + File name + + caption: Optional[str] + Message caption + + Returns + ------- + str + File name + """ + + res: str = "" + for prefix in self.file_name_prefix: + if prefix == "message_id": + if res != "": + res += self.file_name_prefix_split + res += f"{message_id}" + elif prefix == "file_name" and file_name: + if res != "": + res += self.file_name_prefix_split + res += f"{file_name}" + elif prefix == "caption" and caption: + if res != "": + res += self.file_name_prefix_split + res += f"{caption}" + if res == "": + res = f"{message_id}" + + return validate_title(res) + + def need_skip_message( + self, download_config: ChatDownloadConfig, message_id: int + ) -> bool: + """if need skip download message. + + Parameters + ---------- + chat_id: str + Config.yaml defined + + message_id: int + Readily to download message id + Returns + ------- + bool + """ + if message_id in download_config.ids_to_retry_dict: + return True + + return False + + def exec_filter(self, download_config: ChatDownloadConfig, meta_data: MetaData): + """ + Executes the filter on the given download configuration. + + Args: + download_config (ChatDownloadConfig): The download configuration object. + meta_data (MetaData): The meta data object. + + Returns: + bool: The result of executing the filter. + """ + if download_config.download_filter: + self.download_filter.set_meta_data(meta_data) + return self.download_filter.exec(download_config.download_filter) + + return True + + # pylint: disable = R0912 + def update_config(self, immediate: bool = True): + """update config + + Parameters + ---------- + immediate: bool + If update config immediate,default True + """ + # TODO: fix this not exist chat + if not self.app_data.get("chat") and self.config.get("chat"): + self.app_data["chat"] = [ + {"chat_id": i} for i in range(0, len(self.config["chat"])) + ] + idx = 0 + # pylint: disable = R1733 + for key, value in self.chat_download_config.items(): + # pylint: disable = W0201 + unfinished_ids = set(value.ids_to_retry) + + for it in value.ids_to_retry: + if value.node.download_status.get( + it, DownloadStatus.FailedDownload + ) in [DownloadStatus.SuccessDownload, DownloadStatus.SkipDownload]: + unfinished_ids.remove(it) + + for _idx, _value in value.node.download_status.items(): + if DownloadStatus.SuccessDownload != _value and DownloadStatus.SkipDownload != _value: + unfinished_ids.add(_idx) + + self.chat_download_config[key].ids_to_retry = list(unfinished_ids) + + if idx >= len(self.app_data["chat"]): + self.app_data["chat"].append({}) + + if value.finish_task: + self.config["chat"][idx]["last_read_message_id"] = ( + value.last_read_message_id + 1 + ) + + self.app_data["chat"][idx]["chat_id"] = key + self.app_data["chat"][idx]["ids_to_retry"] = value.ids_to_retry + idx += 1 + + self.config["save_path"] = self.save_path + self.config["file_path_prefix"] = self.file_path_prefix + + if self.config.get("ids_to_retry"): + self.config.pop("ids_to_retry") + + if self.config.get("chat_id"): + self.config.pop("chat_id") + + if self.config.get("download_filter"): + self.config.pop("download_filter") + + if self.config.get("last_read_message_id"): + self.config.pop("last_read_message_id") + + self.config["language"] = self.language.name + # for it in self.downloaded_ids: + # self.already_download_ids_set.add(it) + + # self.app_data["already_download_ids"] = list(self.already_download_ids_set) + + if immediate: + with open(self.config_file, "w", encoding="utf-8") as yaml_file: + _yaml.dump(self.config, yaml_file) + + if immediate: + with open(self.app_data_file, "w", encoding="utf-8") as yaml_file: + _yaml.dump(self.app_data, yaml_file) + + def set_language(self, language: Language): + """Set Language""" + self.language = language + set_language(language) + + def is_configured(self) -> bool: + """检查 API 凭证是否已设置,用于判断是否可以连接 Telegram""" + return bool(self.api_id) and bool(self.api_hash) + + def load_config(self): + """Load user config""" + config_path = os.path.join(os.path.abspath("."), self.config_file) + if not os.path.exists(config_path): + # 首次运行,创建默认空配置文件,Web 向导负责填写真实凭证 + default_config = { + "api_id": 0, + "api_hash": "", + "chat": [], + "file_formats": { + "audio": ["all"], + "document": ["all"], + "video": ["all"], + }, + "file_path_prefix": ["chat_title", "media_datetime"], + "media_types": [ + "audio", + "photo", + "video", + "document", + "voice", + "video_note", + ], + "save_path": os.path.join(os.path.abspath("."), "downloads"), + "web_host": "0.0.0.0", + "web_port": int(os.environ.get("WEB_PORT", 5000)), + "language": "CN", + } + with open(config_path, "w", encoding="utf-8") as f: + _yaml.dump(default_config, f) + self.config = default_config + self.assign_config(self.config) + else: + with open(config_path, encoding="utf-8") as f: + config = _yaml.load(f.read()) + if config: + self.config = config + self.assign_config(self.config) + + if os.path.exists(os.path.join(os.path.abspath("."), self.app_data_file)): + with open( + os.path.join(os.path.abspath("."), self.app_data_file), + encoding="utf-8", + ) as f: + app_data = _yaml.load(f.read()) + if app_data: + self.app_data = app_data + self.assign_app_data(self.app_data) + + def pre_run(self): + """before run application do""" + self.cloud_drive_config.pre_run() + if not os.path.exists(self.session_file_path): + os.makedirs(self.session_file_path) + set_language(self.language) + + def set_caption_name( + self, chat_id: Union[int, str], media_group_id: Optional[str], caption: str + ): + """set caption name map + + Parameters + ---------- + chat_id: str + Unique identifier for this chat. + + media_group_id: Optional[str] + The unique identifier of a media message group this message belongs to. + + caption: str + Caption for the audio, document, photo, video or voice, 0-1024 characters. + """ + if not media_group_id: + return + + if chat_id in self.caption_name_dict: + self.caption_name_dict[chat_id][media_group_id] = caption + else: + self.caption_name_dict[chat_id] = {media_group_id: caption} + + def get_caption_name( + self, chat_id: Union[int, str], media_group_id: Optional[str] + ) -> Optional[str]: + """set caption name map + media_group_id: Optional[str] + The unique identifier of a media message group this message belongs to. + + caption: str + Caption for the audio, document, photo, video or voice, 0-1024 characters. + """ + + if ( + not media_group_id + or chat_id not in self.caption_name_dict + or media_group_id not in self.caption_name_dict[chat_id] + ): + return None + + return str(self.caption_name_dict[chat_id][media_group_id]) + + def set_caption_entities( + self, chat_id: Union[int, str], media_group_id: Optional[str], caption_entities + ): + """ + set caption entities map + """ + if not media_group_id: + return + + if chat_id in self.caption_entities_dict: + self.caption_entities_dict[chat_id][media_group_id] = caption_entities + else: + self.caption_entities_dict[chat_id] = {media_group_id: caption_entities} + + def get_caption_entities( + self, chat_id: Union[int, str], media_group_id: Optional[str] + ): + """ + get caption entities map + """ + if ( + not media_group_id + or chat_id not in self.caption_entities_dict + or media_group_id not in self.caption_entities_dict[chat_id] + ): + return None + + return self.caption_entities_dict[chat_id][media_group_id] + + def set_download_id( + self, node: TaskNode, message_id: int, download_status: DownloadStatus + ): + """Set Download status""" + if download_status is DownloadStatus.SuccessDownload: + self.total_download_task += 1 + + if node.chat_id not in self.chat_download_config: + return + + self.chat_download_config[node.chat_id].finish_task += 1 + + self.chat_download_config[node.chat_id].last_read_message_id = max( + self.chat_download_config[node.chat_id].last_read_message_id, message_id + ) diff --git a/module/bot.py b/module/bot.py new file mode 100644 index 0000000..5ccc723 --- /dev/null +++ b/module/bot.py @@ -0,0 +1,1168 @@ +"""Bot for media downloader""" + +import asyncio +import os +from datetime import datetime +from typing import Callable, List, Union + +import pyrogram +from loguru import logger +from pyrogram import types +from pyrogram.handlers import CallbackQueryHandler, MessageHandler +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from ruamel import yaml + +import utils +from module.app import ( + Application, + ChatDownloadConfig, + ForwardStatus, + QueryHandler, + QueryHandlerStr, + TaskNode, + TaskType, + UploadStatus, +) +from module.filter import Filter +from module.get_chat_history_v2 import get_chat_history_v2 +from module.language import Language, _t +from module.pyrogram_extension import ( + check_user_permission, + parse_link, + proc_cache_forward, + report_bot_forward_status, + report_bot_status, + retry, + set_meta_data, + upload_telegram_chat_message, +) +from utils.format import replace_date_time, validate_title +from utils.meta_data import MetaData + +# pylint: disable = C0301, R0902 + + +class DownloadBot: + """Download bot""" + + def __init__(self): + self.bot = None + self.client = None + self.add_download_task: Callable = None + self.download_chat_task: Callable = None + self.app = None + self.listen_forward_chat: dict = {} + self.config: dict = {} + self._yaml = yaml.YAML() + self.config_path = os.path.join(os.path.abspath("."), "bot.yaml") + self.download_command: dict = {} + self.filter = Filter() + self.bot_info = None + self.task_node: dict = {} + self.is_running = True + self.allowed_user_ids: List[Union[int, str]] = [] + + meta = MetaData(datetime(2022, 8, 5, 14, 35, 12), 0, "", 0, 0, 0, "", 0) + self.filter.set_meta_data(meta) + + self.download_filter: List[str] = [] + self.task_id: int = 0 + self.reply_task = None + + def gen_task_id(self) -> int: + """Gen task id""" + self.task_id += 1 + return self.task_id + + def add_task_node(self, node: TaskNode): + """Add task node""" + self.task_node[node.task_id] = node + + def remove_task_node(self, task_id: int): + """Remove task node""" + self.task_node.pop(task_id) + + def stop_task(self, task_id: str): + """Stop task""" + if task_id == "all": + for value in self.task_node.values(): + value.stop_transmission() + else: + try: + task = self.task_node.get(int(task_id)) + if task: + task.stop_transmission() + except Exception: + return + + async def update_reply_message(self): + """Update reply message""" + while self.is_running: + for key, value in self.task_node.copy().items(): + if value.is_running: + await report_bot_status(self.bot, value) + + for key, value in self.task_node.copy().items(): + if value.is_running and value.is_finish(): + self.remove_task_node(key) + await asyncio.sleep(3) + + def assign_config(self, _config: dict): + """assign config from str. + + Parameters + ---------- + _config: dict + application config dict + + Returns + ------- + bool + """ + + self.download_filter = _config.get("download_filter", self.download_filter) + + return True + + def update_config(self): + """Update config from str.""" + self.config["download_filter"] = self.download_filter + + with open("d", "w", encoding="utf-8") as yaml_file: + self._yaml.dump(self.config, yaml_file) + + async def start( + self, + app: Application, + client: pyrogram.Client, + add_download_task: Callable, + download_chat_task: Callable, + ): + """Start bot""" + self.bot = pyrogram.Client( + app.application_name + "_bot", + api_hash=app.api_hash, + api_id=app.api_id, + bot_token=app.bot_token, + workdir=app.session_file_path, + proxy=app.proxy, + ) + + # 命令列表 + commands = [ + types.BotCommand("help", _t("Help")), + types.BotCommand( + "get_info", _t("Get group and user info from message link") + ), + types.BotCommand( + "download", + _t( + "To download the video, use the method to directly enter /download to view" + ), + ), + types.BotCommand( + "forward", + _t("Forward video, use the method to directly enter /forward to view"), + ), + types.BotCommand( + "listen_forward", + _t( + "Listen forward, use the method to directly enter /listen_forward to view" + ), + ), + types.BotCommand( + "add_filter", + _t( + "Add download filter, use the method to directly enter /add_filter to view" + ), + ), + types.BotCommand("set_language", _t("Set language")), + types.BotCommand("stop", _t("Stop bot download or forward")), + ] + + self.app = app + self.client = client + self.add_download_task = add_download_task + self.download_chat_task = download_chat_task + + # load config + if os.path.exists(self.config_path): + with open(self.config_path, encoding="utf-8") as f: + config = self._yaml.load(f.read()) + if config: + self.config = config + self.assign_config(self.config) + + await self.bot.start() + + self.bot_info = await self.bot.get_me() + + for allowed_user_id in self.app.allowed_user_ids: + try: + chat = await self.client.get_chat(allowed_user_id) + self.allowed_user_ids.append(chat.id) + except Exception as e: + logger.warning(f"set allowed_user_ids error: {e}") + + admin = await self.client.get_me() + self.allowed_user_ids.append(admin.id) + + await self.bot.set_bot_commands(commands) + + self.bot.add_handler( + MessageHandler( + download_from_bot, + filters=pyrogram.filters.command(["download"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + forward_messages, + filters=pyrogram.filters.command(["forward"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + download_forward_media, + filters=pyrogram.filters.media + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + download_from_link, + filters=pyrogram.filters.regex(r"^https://t.me.*") + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + set_listen_forward_msg, + filters=pyrogram.filters.command(["listen_forward"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + help_command, + filters=pyrogram.filters.command(["help"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + get_info, + filters=pyrogram.filters.command(["get_info"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + help_command, + filters=pyrogram.filters.command(["start"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + set_language, + filters=pyrogram.filters.command(["set_language"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + self.bot.add_handler( + MessageHandler( + add_filter, + filters=pyrogram.filters.command(["add_filter"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + + self.bot.add_handler( + MessageHandler( + stop, + filters=pyrogram.filters.command(["stop"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + + self.bot.add_handler( + CallbackQueryHandler( + on_query_handler, filters=pyrogram.filters.user(self.allowed_user_ids) + ) + ) + + self.client.add_handler(MessageHandler(listen_forward_msg)) + + try: + await send_help_str(self.bot, admin.id) + except Exception: + pass + + self.reply_task = _bot.app.loop.create_task(_bot.update_reply_message()) + + self.bot.add_handler( + MessageHandler( + forward_to_comments, + filters=pyrogram.filters.command(["forward_to_comments"]) + & pyrogram.filters.user(self.allowed_user_ids), + ) + ) + + +_bot = DownloadBot() + + +async def start_download_bot( + app: Application, + client: pyrogram.Client, + add_download_task: Callable, + download_chat_task: Callable, +): + """Start download bot""" + await _bot.start(app, client, add_download_task, download_chat_task) + + +async def stop_download_bot(): + """Stop download bot""" + _bot.update_config() + _bot.is_running = False + if _bot.reply_task: + _bot.reply_task.cancel() + _bot.stop_task("all") + if _bot.bot: + await _bot.bot.stop() + + +async def send_help_str(client: pyrogram.Client, chat_id): + """ + Sends a help string to the specified chat ID using the provided client. + + Parameters: + client (pyrogram.Client): The Pyrogram client used to send the message. + chat_id: The ID of the chat to which the message will be sent. + + Returns: + str: The help string that was sent. + + Note: + The help string includes information about the Telegram Media Downloader bot, + its version, and the available commands. + """ + + update_keyboard = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Github", + url="https://github.com/tangyoha/telegram_media_downloader/releases", + ), + InlineKeyboardButton( + "Join us", url="https://t.me/TeegramMediaDownload" + ), + ] + ] + ) + latest_release_str = "" + # try: + # latest_release = get_latest_release(_bot.app.proxy) + + # latest_release_str = ( + # f"{_t('New Version')}: [{latest_release['name']}]({latest_release['html_url']})\an" + # if latest_release + # else "" + # ) + # except Exception: + # latest_release_str = "" + + msg = ( + f"`\n🤖 {_t('Telegram Media Downloader')}\n" + f"🌐 {_t('Version')}: {utils.__version__}`\n" + f"{latest_release_str}\n" + f"{_t('Available commands:')}\n" + f"/help - {_t('Show available commands')}\n" + f"/get_info - {_t('Get group and user info from message link')}\n" + f"/download - {_t('Download messages')}\n" + f"/forward - {_t('Forward messages')}\n" + f"/listen_forward - {_t('Listen for forwarded messages')}\n" + f"/forward_to_comments - {_t('Forward a specific media to a comment section')}\n" + f"/set_language - {_t('Set language')}\n" + f"/stop - {_t('Stop bot download or forward')}\n\n" + f"{_t('**Note**: 1 means the start of the entire chat')}," + f"{_t('0 means the end of the entire chat')}\n" + f"`[` `]` {_t('means optional, not required')}\n" + ) + + await client.send_message(chat_id, msg, reply_markup=update_keyboard) + + +async def help_command(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Sends a message with the available commands and their usage. + + Parameters: + client (pyrogram.Client): The client instance. + message (pyrogram.types.Message): The message object. + + Returns: + None + """ + + await send_help_str(client, message.chat.id) + + +async def set_language(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Set the language of the bot. + + Parameters: + client (pyrogram.Client): The pyrogram client. + message (pyrogram.types.Message): The message containing the command. + + Returns: + None + """ + + if len(message.text.split()) != 2: + await client.send_message( + message.from_user.id, + _t("Invalid command format. Please use /set_language en/ru/zh/ua"), + ) + return + + language = message.text.split()[1] + + try: + language = Language[language.upper()] + _bot.app.set_language(language) + await client.send_message( + message.from_user.id, f"{_t('Language set to')} {language.name}" + ) + except KeyError: + await client.send_message( + message.from_user.id, + _t("Invalid command format. Please use /set_language en/ru/zh/ua"), + ) + + +async def get_info(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Async function that retrieves information from a group message link. + """ + + msg = _t("Invalid command format. Please use /get_info group_message_link") + + args = message.text.split() + if len(args) != 2: + await client.send_message( + message.from_user.id, + msg, + ) + return + + chat_id, message_id, _ = await parse_link(_bot.client, args[1]) + + entity = None + if chat_id: + entity = await _bot.client.get_chat(chat_id) + + if entity: + if message_id: + _message = await retry(_bot.client.get_messages, args=(chat_id, message_id)) + if _message: + meta_data = MetaData() + set_meta_data(meta_data, _message) + msg = ( + f"`\n" + f"{_t('Group/Channel')}\n" + f"├─ {_t('id')}: {entity.id}\n" + f"├─ {_t('first name')}: {entity.first_name}\n" + f"├─ {_t('last name')}: {entity.last_name}\n" + f"└─ {_t('name')}: {entity.username}\n" + f"{_t('Message')}\n" + ) + + for key, value in meta_data.data().items(): + if key == "send_name": + msg += f"└─ {key}: {value or None}\n" + else: + msg += f"├─ {key}: {value or None}\n" + + msg += "`" + await client.send_message( + message.from_user.id, + msg, + ) + + +async def add_filter(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Set the download filter of the bot. + + Parameters: + client (pyrogram.Client): The pyrogram client. + message (pyrogram.types.Message): The message containing the command. + + Returns: + None + """ + + args = message.text.split(maxsplit=1) + if len(args) != 2: + await client.send_message( + message.from_user.id, + _t("Invalid command format. Please use /add_filter your filter"), + ) + return + + filter_str = replace_date_time(args[1]) + res, err = _bot.filter.check_filter(filter_str) + if res: + _bot.app.down = args[1] + await client.send_message( + message.from_user.id, f"{_t('Add download filter')} : {args[1]}" + ) + else: + await client.send_message( + message.from_user.id, f"{err}\n{_t('Check error, please add again!')}" + ) + return + + +async def direct_download( + download_bot: DownloadBot, + chat_id: Union[str, int], + message: pyrogram.types.Message, + download_message: pyrogram.types.Message, + client: pyrogram.Client = None, +): + """Direct Download""" + + replay_message = "Direct download..." + last_reply_message = await download_bot.bot.send_message( + message.from_user.id, replay_message, reply_to_message_id=message.id + ) + + node = TaskNode( + chat_id=chat_id, + from_user_id=message.from_user.id, + reply_message_id=last_reply_message.id, + replay_message=replay_message, + limit=1, + bot=download_bot.bot, + task_id=_bot.gen_task_id(), + ) + + node.client = client + + _bot.add_task_node(node) + + await _bot.add_download_task( + download_message, + node, + ) + + node.is_running = True + + +async def download_forward_media( + client: pyrogram.Client, message: pyrogram.types.Message +): + """ + Downloads the media from a forwarded message. + + Parameters: + client (pyrogram.Client): The client instance. + message (pyrogram.types.Message): The message object. + + Returns: + None + """ + + if message.media and getattr(message, message.media.value): + await direct_download(_bot, message.from_user.id, message, message, client) + return + + await client.send_message( + message.from_user.id, + f"1. {_t('Direct download, directly forward the message to your robot')}\n\n", + parse_mode=pyrogram.enums.ParseMode.HTML, + ) + + +async def download_from_link(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Downloads a single message from a Telegram link. + + Parameters: + client (pyrogram.Client): The pyrogram client. + message (pyrogram.types.Message): The message containing the Telegram link. + + Returns: + None + """ + + if not message.text or not message.text.startswith("https://t.me"): + return + + msg = ( + f"1. {_t('Directly download a single message')}\n" + "https://t.me/12000000/1\n\n" + ) + + text = message.text.split() + if len(text) != 1: + await client.send_message( + message.from_user.id, msg, parse_mode=pyrogram.enums.ParseMode.HTML + ) + + chat_id, message_id, _ = await parse_link(_bot.client, text[0]) + + entity = None + if chat_id: + entity = await _bot.client.get_chat(chat_id) + if entity: + if message_id: + download_message = await retry( + _bot.client.get_messages, args=(chat_id, message_id) + ) + if download_message: + await direct_download(_bot, entity.id, message, download_message) + else: + client.send_message( + message.from_user.id, + f"{_t('From')} {entity.title} {_t('download')} {message_id} {_t('error')}!", + reply_to_message_id=message.id, + ) + return + + await client.send_message( + message.from_user.id, msg, parse_mode=pyrogram.enums.ParseMode.HTML + ) + + +# pylint: disable = R0912, R0915,R0914 + + +async def download_from_bot(client: pyrogram.Client, message: pyrogram.types.Message): + """Download from bot""" + + msg = ( + f"{_t('Parameter error, please enter according to the reference format')}:\n\n" + f"1. {_t('Download all messages of common group')}\n" + "/download https://t.me/fkdhlg 1 0\n\n" + f"{_t('The private group (channel) link is a random group message link')}\n\n" + f"2. {_t('The download starts from the N message to the end of the M message')}. " + f"{_t('When M is 0, it means the last message. The filter is optional')}\n" + f"/download https://t.me/12000000 N M [filter]\n\n" + ) + + args = message.text.split(maxsplit=4) + if not message.text or len(args) < 4: + await client.send_message( + message.from_user.id, msg, parse_mode=pyrogram.enums.ParseMode.HTML + ) + return + + url = args[1] + try: + start_offset_id = int(args[2]) + end_offset_id = int(args[3]) + except Exception: + await client.send_message( + message.from_user.id, msg, parse_mode=pyrogram.enums.ParseMode.HTML + ) + return + + limit = 0 + if end_offset_id: + if end_offset_id < start_offset_id: + raise ValueError( + f"end_offset_id < start_offset_id, {end_offset_id} < {start_offset_id}" + ) + + limit = end_offset_id - start_offset_id + 1 + + download_filter = args[4] if len(args) > 4 else None + + if download_filter: + download_filter = replace_date_time(download_filter) + res, err = _bot.filter.check_filter(download_filter) + if not res: + await client.send_message( + message.from_user.id, err, reply_to_message_id=message.id + ) + return + try: + chat_id, _, _ = await parse_link(_bot.client, url) + if chat_id: + entity = await _bot.client.get_chat(chat_id) + if entity: + chat_title = entity.title + reply_message = f"from {chat_title} " + chat_download_config = ChatDownloadConfig() + chat_download_config.last_read_message_id = start_offset_id + chat_download_config.download_filter = download_filter + reply_message += ( + f"download message id = {start_offset_id} - {end_offset_id} !" + ) + last_reply_message = await client.send_message( + message.from_user.id, reply_message, reply_to_message_id=message.id + ) + node = TaskNode( + chat_id=entity.id, + from_user_id=message.from_user.id, + reply_message_id=last_reply_message.id, + replay_message=reply_message, + limit=limit, + start_offset_id=start_offset_id, + end_offset_id=end_offset_id, + bot=_bot.bot, + task_id=_bot.gen_task_id(), + ) + _bot.add_task_node(node) + _bot.app.loop.create_task( + _bot.download_chat_task(_bot.client, chat_download_config, node) + ) + except Exception as e: + await client.send_message( + message.from_user.id, + f"{_t('chat input error, please enter the channel or group link')}\n\n" + f"{_t('Error type')}: {e.__class__}" + f"{_t('Exception message')}: {e}", + ) + return + + +async def get_forward_task_node( + client: pyrogram.Client, + message: pyrogram.types.Message, + task_type: TaskType, + src_chat_link: str, + dst_chat_link: str, + offset_id: int = 0, + end_offset_id: int = 0, + download_filter: str = None, + reply_comment: bool = False, +): + """Get task node""" + limit: int = 0 + + if end_offset_id: + if end_offset_id < offset_id: + await client.send_message( + message.from_user.id, + f" end_offset_id({end_offset_id}) < start_offset_id({offset_id})," + f" end_offset_id{_t('must be greater than')} offset_id", + ) + return None + + limit = end_offset_id - offset_id + 1 + + src_chat_id, _, _ = await parse_link(_bot.client, src_chat_link) + dst_chat_id, target_msg_id, topic_id = await parse_link(_bot.client, dst_chat_link) + + if not src_chat_id or not dst_chat_id: + logger.info(f"{src_chat_id} {dst_chat_id}") + await client.send_message( + message.from_user.id, + _t("Invalid chat link") + f"{src_chat_id} {dst_chat_id}", + reply_to_message_id=message.id, + ) + return None + + try: + src_chat = await _bot.client.get_chat(src_chat_id) + dst_chat = await _bot.client.get_chat(dst_chat_id) + except Exception as e: + await client.send_message( + message.from_user.id, + f"{_t('Invalid chat link')} {e}", + reply_to_message_id=message.id, + ) + logger.exception(f"get chat error: {e}") + return None + + me = await client.get_me() + if dst_chat.id == me.id: + # TODO: when bot receive message judge if download + await client.send_message( + message.from_user.id, + _t("Cannot be forwarded to this bot, will cause an infinite loop"), + reply_to_message_id=message.id, + ) + return None + + if download_filter: + download_filter = replace_date_time(download_filter) + res, err = _bot.filter.check_filter(download_filter) + if not res: + await client.send_message( + message.from_user.id, err, reply_to_message_id=message.id + ) + + last_reply_message = await client.send_message( + message.from_user.id, + "Forwarding message, please wait...", + reply_to_message_id=message.id, + ) + + node = TaskNode( + chat_id=src_chat.id, + from_user_id=message.from_user.id, + upload_telegram_chat_id=dst_chat_id, + reply_message_id=last_reply_message.id, + replay_message=last_reply_message.text, + has_protected_content=src_chat.has_protected_content, + download_filter=download_filter, + limit=limit, + start_offset_id=offset_id, + end_offset_id=end_offset_id, + bot=_bot.bot, + task_id=_bot.gen_task_id(), + task_type=task_type, + topic_id=topic_id, + ) + + if target_msg_id and reply_comment: + node.reply_to_message = await _bot.client.get_discussion_message( + dst_chat_id, target_msg_id + ) + + _bot.add_task_node(node) + + node.upload_user = _bot.client + if not dst_chat.type is pyrogram.enums.ChatType.BOT: + has_permission = await check_user_permission(_bot.client, me.id, dst_chat.id) + if has_permission: + node.upload_user = _bot.bot + + if node.upload_user is _bot.client: + await client.edit_message_text( + message.from_user.id, + last_reply_message.id, + "Note that the robot may not be in the target group," + " use the user account to forward", + ) + + return node + + +# pylint: disable = R0914 +async def forward_message_impl( + client: pyrogram.Client, message: pyrogram.types.Message, reply_comment: bool +): + """ + Forward message + """ + + async def report_error(client: pyrogram.Client, message: pyrogram.types.Message): + """Report error""" + + await client.send_message( + message.from_user.id, + f"{_t('Invalid command format')}." + f"{_t('Please use')} " + "/forward https://t.me/c/src_chat https://t.me/c/dst_chat " + f"1 400 `[`{_t('Filter')}`]`\n", + ) + + args = message.text.split(maxsplit=5) + if len(args) < 5: + await report_error(client, message) + return + + src_chat_link = args[1] + dst_chat_link = args[2] + + try: + offset_id = int(args[3]) + end_offset_id = int(args[4]) + except Exception: + await report_error(client, message) + return + + download_filter = args[5] if len(args) > 5 else None + + node = await get_forward_task_node( + client, + message, + TaskType.Forward, + src_chat_link, + dst_chat_link, + offset_id, + end_offset_id, + download_filter, + reply_comment, + ) + + if not node: + return + + if not node.has_protected_content: + try: + async for item in get_chat_history_v2( # type: ignore + _bot.client, + node.chat_id, + limit=node.limit, + max_id=node.end_offset_id, + offset_id=offset_id, + reverse=True, + ): + await forward_normal_content(client, node, item) + if node.is_stop_transmission: + await client.edit_message_text( + message.from_user.id, + node.reply_message_id, + f"{_t('Stop Forward')}", + ) + break + except Exception as e: + await client.edit_message_text( + message.from_user.id, + node.reply_message_id, + f"{_t('Error forwarding message')} {e}", + ) + finally: + await report_bot_status(client, node, immediate_reply=True) + node.stop_transmission() + else: + await forward_msg(node, offset_id) + + +async def forward_messages(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Forwards messages from one chat to another. + + Parameters: + client (pyrogram.Client): The pyrogram client. + message (pyrogram.types.Message): The message containing the command. + + Returns: + None + """ + return await forward_message_impl(client, message, False) + + +async def forward_normal_content( + client: pyrogram.Client, node: TaskNode, message: pyrogram.types.Message +): + """Forward normal content""" + forward_ret = ForwardStatus.FailedForward + if node.download_filter: + meta_data = MetaData() + caption = message.caption + if caption: + caption = validate_title(caption) + _bot.app.set_caption_name(node.chat_id, message.media_group_id, caption) + else: + caption = _bot.app.get_caption_name(node.chat_id, message.media_group_id) + set_meta_data(meta_data, message, caption) + _bot.filter.set_meta_data(meta_data) + if not _bot.filter.exec(node.download_filter): + forward_ret = ForwardStatus.SkipForward + if message.media_group_id: + node.upload_status[message.id] = UploadStatus.SkipUpload + await proc_cache_forward(_bot.client, node, message, False) + await report_bot_forward_status(client, node, forward_ret) + return + + await upload_telegram_chat_message( + _bot.client, node.upload_user, _bot.app, node, message + ) + + +async def forward_msg(node: TaskNode, message_id: int): + """Forward normal message""" + + chat_download_config = ChatDownloadConfig() + chat_download_config.last_read_message_id = message_id + chat_download_config.download_filter = node.download_filter # type: ignore + + await _bot.download_chat_task(_bot.client, chat_download_config, node) + + +async def set_listen_forward_msg( + client: pyrogram.Client, message: pyrogram.types.Message +): + """ + Set the chat to listen for forwarded messages. + + Args: + client (pyrogram.Client): The pyrogram client. + message (pyrogram.types.Message): The message sent by the user. + + Returns: + None + """ + args = message.text.split(maxsplit=3) + + if len(args) < 3: + await client.send_message( + message.from_user.id, + f"{_t('Invalid command format')}. {_t('Please use')} /listen_forward " + f"https://t.me/c/src_chat https://t.me/c/dst_chat [{_t('Filter')}]\n", + ) + return + + src_chat_link = args[1] + dst_chat_link = args[2] + + download_filter = args[3] if len(args) > 3 else None + + node = await get_forward_task_node( + client, + message, + TaskType.ListenForward, + src_chat_link, + dst_chat_link, + download_filter=download_filter, + ) + + if not node: + return + + if node.chat_id in _bot.listen_forward_chat: + _bot.remove_task_node(_bot.listen_forward_chat[node.chat_id].task_id) + + node.is_running = True + _bot.listen_forward_chat[node.chat_id] = node + + +async def listen_forward_msg(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Forwards messages from a chat to another chat if the message does not contain protected content. + If the message contains protected content, it will be downloaded and forwarded to the other chat. + + Parameters: + client (pyrogram.Client): The pyrogram client. + message (pyrogram.types.Message): The message to be forwarded. + """ + + if message.chat and message.chat.id in _bot.listen_forward_chat: + node = _bot.listen_forward_chat[message.chat.id] + + # TODO(tangyoha):fix run time change protected content + if not node.has_protected_content: + await forward_normal_content(client, node, message) + await report_bot_status(client, node, immediate_reply=True) + else: + await _bot.add_download_task( + message, + node, + ) + + +async def stop(client: pyrogram.Client, message: pyrogram.types.Message): + """Stops listening for forwarded messages.""" + + await client.send_message( + message.chat.id, + _t("Please select:"), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + _t("Stop Download"), callback_data="stop_download" + ), + InlineKeyboardButton( + _t("Stop Forward"), callback_data="stop_forward" + ), + ], + [ # Second row + InlineKeyboardButton( + _t("Stop Listen Forward"), callback_data="stop_listen_forward" + ) + ], + ] + ), + ) + + +async def stop_task( + client: pyrogram.Client, + query: pyrogram.types.CallbackQuery, + queryHandler: str, + task_type: TaskType, +): + """Stop task""" + if query.data == queryHandler: + buttons: List[InlineKeyboardButton] = [] + temp_buttons: List[InlineKeyboardButton] = [] + for key, value in _bot.task_node.copy().items(): + if not value.is_finish() and value.task_type is task_type: + if len(temp_buttons) == 3: + buttons.append(temp_buttons) + temp_buttons = [] + temp_buttons.append( + InlineKeyboardButton( + f"{key}", callback_data=f"{queryHandler} task {key}" + ) + ) + if temp_buttons: + buttons.append(temp_buttons) + + if buttons: + buttons.insert( + 0, + [ + InlineKeyboardButton( + _t("all"), callback_data=f"{queryHandler} task all" + ) + ], + ) + await client.edit_message_text( + query.message.from_user.id, + query.message.id, + f"{_t('Stop')} {_t(task_type.name)}...", + reply_markup=InlineKeyboardMarkup(buttons), + ) + else: + await client.edit_message_text( + query.message.from_user.id, + query.message.id, + f"{_t('No Task')}", + ) + else: + task_id = query.data.split(" ")[2] + await client.edit_message_text( + query.message.from_user.id, + query.message.id, + f"{_t('Stop')} {_t(task_type.name)}...", + ) + _bot.stop_task(task_id) + + +async def on_query_handler( + client: pyrogram.Client, query: pyrogram.types.CallbackQuery +): + """ + Asynchronous function that handles query callbacks. + + Parameters: + client (pyrogram.Client): The Pyrogram client object. + query (pyrogram.types.CallbackQuery): The callback query object. + + Returns: + None + """ + + for it in QueryHandler: + queryHandler = QueryHandlerStr.get_str(it.value) + if queryHandler in query.data: + await stop_task(client, query, queryHandler, TaskType(it.value)) + + +async def forward_to_comments(client: pyrogram.Client, message: pyrogram.types.Message): + """ + Forwards specified media to a designated comment section. + + Usage: /forward_to_comments + + Parameters: + client (pyrogram.Client): The pyrogram client. + message (pyrogram.types.Message): The message containing the command. + """ + return await forward_message_impl(client, message, True) diff --git a/module/cloud_drive.py b/module/cloud_drive.py new file mode 100644 index 0000000..8f2684d --- /dev/null +++ b/module/cloud_drive.py @@ -0,0 +1,254 @@ +"""provide upload cloud drive""" +import asyncio +import functools +import importlib +import inspect +import os +import re +from asyncio import subprocess +from subprocess import Popen +from typing import Callable +from zipfile import ZipFile + +from loguru import logger + +from utils import platform + + +# pylint: disable = R0902 +class CloudDriveConfig: + """Rclone Config""" + + def __init__( + self, + enable_upload_file: bool = False, + before_upload_file_zip: bool = False, + after_upload_file_delete: bool = True, + rclone_path: str = os.path.join( + os.path.abspath("."), "rclone", f"rclone{platform.get_exe_ext()}" + ), + remote_dir: str = "", + upload_adapter: str = "rclone", + ): + self.enable_upload_file = enable_upload_file + self.before_upload_file_zip = before_upload_file_zip + self.after_upload_file_delete = after_upload_file_delete + self.rclone_path = rclone_path + self.remote_dir = remote_dir + self.upload_adapter = upload_adapter + self.dir_cache: dict = {} # for remote mkdir + self.total_upload_success_file_count = 0 + self.aligo = None + + def pre_run(self): + """pre run init aligo""" + if self.enable_upload_file and self.upload_adapter == "aligo": + CloudDrive.init_upload_adapter(self) + + +class CloudDrive: + """rclone support""" + + @staticmethod + def init_upload_adapter(drive_config: CloudDriveConfig): + """Initialize the upload adapter.""" + if drive_config.upload_adapter == "aligo": + Aligo = importlib.import_module("aligo").Aligo + drive_config.aligo = Aligo() + + @staticmethod + def rclone_mkdir(drive_config: CloudDriveConfig, remote_dir: str): + """mkdir in remote""" + with Popen( + f'"{drive_config.rclone_path}" mkdir "{remote_dir}/"', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ): + pass + + @staticmethod + def aligo_mkdir(drive_config: CloudDriveConfig, remote_dir: str): + """mkdir in remote by aligo""" + if drive_config.aligo and not drive_config.aligo.get_folder_by_path(remote_dir): + drive_config.aligo.create_folder(name=remote_dir, check_name_mode="refuse") + + @staticmethod + def zip_file(local_file_path: str) -> str: + """ + Zip local file + """ + + file_path_without_extension = os.path.splitext(local_file_path)[0] + zip_file_name = file_path_without_extension + ".zip" + + with ZipFile(zip_file_name, "w") as zip_writer: + zip_writer.write(local_file_path) + + return zip_file_name + + # pylint: disable = R0914 + @staticmethod + async def rclone_upload_file( + drive_config: CloudDriveConfig, + save_path: str, + local_file_path: str, + progress_callback: Callable = None, + progress_args: tuple = (), + ) -> bool: + """Use Rclone upload file""" + upload_status: bool = False + try: + remote_dir = ( + drive_config.remote_dir + + "/" + + os.path.dirname(local_file_path).replace(save_path, "") + + "/" + ).replace("\\", "/") + + if not drive_config.dir_cache.get(remote_dir): + CloudDrive.rclone_mkdir(drive_config, remote_dir) + drive_config.dir_cache[remote_dir] = True + + zip_file_path: str = "" + file_path = local_file_path + if drive_config.before_upload_file_zip: + zip_file_path = CloudDrive.zip_file(local_file_path) + file_path = zip_file_path + else: + file_path = local_file_path + + cmd = ( + f'"{drive_config.rclone_path}" copy "{file_path}" ' + f'"{remote_dir}/" --create-empty-src-dirs --ignore-existing --progress' + ) + proc = await asyncio.create_subprocess_shell( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + if proc.stdout: + async for output in proc.stdout: + s = output.decode(errors="replace") + print(s) + if "Transferred" in s and "100%" in s and "1 / 1" in s: + logger.info(f"upload file {local_file_path} success") + drive_config.total_upload_success_file_count += 1 + if drive_config.after_upload_file_delete: + os.remove(local_file_path) + if drive_config.before_upload_file_zip: + os.remove(zip_file_path) + upload_status = True + else: + pattern = ( + r"Transferred: (.*?) / (.*?), (.*?)%, (.*?/s)?, ETA (.*?)$" + ) + transferred_match = re.search(pattern, s) + + if transferred_match: + if progress_callback: + func = functools.partial( + progress_callback, + transferred_match.group(1), + transferred_match.group(2), + transferred_match.group(3), + transferred_match.group(4), + transferred_match.group(5), + *progress_args, + ) + + if inspect.iscoroutinefunction(progress_callback): + await func() + + await proc.wait() + except Exception as e: + logger.error(f"{e.__class__} {e}") + return False + + return upload_status + + @staticmethod + def aligo_upload_file( + drive_config: CloudDriveConfig, save_path: str, local_file_path: str + ): + """aliyun upload file""" + upload_status: bool = False + if not drive_config.aligo: + logger.warning("please config aligo! see README.md") + return False + + try: + remote_dir = ( + drive_config.remote_dir + + "/" + + os.path.dirname(local_file_path).replace(save_path, "") + + "/" + ).replace("\\", "/") + + if not drive_config.dir_cache.get(remote_dir): + CloudDrive.aligo_mkdir(drive_config, remote_dir) + aligo_dir = drive_config.aligo.get_folder_by_path(remote_dir) + if aligo_dir: + drive_config.dir_cache[remote_dir] = aligo_dir.file_id + + zip_file_path: str = "" + file_paths = [] + if drive_config.before_upload_file_zip: + zip_file_path = CloudDrive.zip_file(local_file_path) + file_paths.append(zip_file_path) + else: + file_paths.append(local_file_path) + + res = drive_config.aligo.upload_files( + file_paths=file_paths, + parent_file_id=drive_config.dir_cache[remote_dir], + check_name_mode="refuse", + ) + + if len(res) > 0: + drive_config.total_upload_success_file_count += len(res) + if drive_config.after_upload_file_delete: + os.remove(local_file_path) + + if drive_config.before_upload_file_zip: + os.remove(zip_file_path) + + upload_status = True + + except Exception as e: + logger.error(f"{e.__class__} {e}") + return False + + return upload_status + + @staticmethod + async def upload_file( + drive_config: CloudDriveConfig, save_path: str, local_file_path: str + ) -> bool: + """Upload file + Parameters + ---------- + drive_config: CloudDriveConfig + see @CloudDriveConfig + + save_path: str + Local file save path config + + local_file_path: str + Local file path + + Returns + ------- + bool + True or False + """ + if not drive_config.enable_upload_file: + return False + + ret: bool = False + if drive_config.upload_adapter == "rclone": + ret = await CloudDrive.rclone_upload_file( + drive_config, save_path, local_file_path + ) + elif drive_config.upload_adapter == "aligo": + ret = CloudDrive.aligo_upload_file(drive_config, save_path, local_file_path) + + return ret diff --git a/module/database.py b/module/database.py new file mode 100644 index 0000000..d1f39cb --- /dev/null +++ b/module/database.py @@ -0,0 +1,275 @@ +"""SQLite 下载历史数据库封装""" + +import hashlib +import json +import os +import re +import sqlite3 +import time +from typing import Optional + +_conn: Optional[sqlite3.Connection] = None + + +def init_db(db_path: str): + """初始化数据库,创建表结构。在 main() 中调用一次。""" + global _conn + os.makedirs(os.path.dirname(db_path), exist_ok=True) + _conn = sqlite3.connect(db_path, check_same_thread=False) + _conn.row_factory = sqlite3.Row + _conn.execute(""" + CREATE TABLE IF NOT EXISTS downloads ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id TEXT NOT NULL, + chat_title TEXT, + message_id INTEGER NOT NULL, + file_name TEXT, + file_path TEXT, + file_size INTEGER, + media_type TEXT, + download_time TEXT DEFAULT (datetime('now','localtime')), + status TEXT, + UNIQUE(chat_id, message_id) + ) + """) + # 预扫描结果缓存表:(chat_id, filter_key) → estimated_total + _conn.execute(""" + CREATE TABLE IF NOT EXISTS scan_cache ( + chat_id TEXT NOT NULL, + filter_key TEXT NOT NULL, + estimated_total INTEGER NOT NULL, + scanned_at INTEGER NOT NULL, + PRIMARY KEY (chat_id, filter_key) + ) + """) + _conn.commit() + + +def is_downloaded(chat_id: str, message_id: int) -> bool: + """检查某条消息是否已成功下载过(status='success')。""" + if _conn is None: + return False + cur = _conn.execute( + "SELECT 1 FROM downloads WHERE chat_id=? AND message_id=? AND status='success'", + (chat_id, message_id), + ) + return cur.fetchone() is not None + + +def should_skip(chat_id: str, message_id: int) -> tuple: + """检查是否应跳过该消息(已成功下载或用户手动跳过)。 + 返回 (should_skip: bool, reason: str) + """ + if _conn is None: + return False, "" + cur = _conn.execute( + "SELECT status FROM downloads WHERE chat_id=? AND message_id=?", + (chat_id, message_id), + ) + row = cur.fetchone() + if row is None: + return False, "" + if row["status"] == "success": + return True, "曾经下载过(本地已删除)" + if row["status"] == "skip": + return True, "用户已手动跳过" + return False, "" + + +def record_skip(chat_id: str, chat_title: str, message_id: int, file_name: str = ""): + """记录用户手动跳过的消息。""" + if _conn is None: + return + _conn.execute( + """ + INSERT INTO downloads + (chat_id, chat_title, message_id, file_name, file_path, file_size, media_type, + download_time, status) + VALUES (?, ?, ?, ?, '', 0, '', datetime('now','localtime'), 'skip') + ON CONFLICT(chat_id, message_id) DO UPDATE SET + chat_title=excluded.chat_title, + download_time=excluded.download_time, + status='skip' + """, + (chat_id, chat_title, message_id, file_name), + ) + _conn.commit() + + +def delete_record(chat_id: str, message_id: int): + """删除下载记录(撤销跳过或重新下载)。""" + if _conn is None: + return + _conn.execute( + "DELETE FROM downloads WHERE chat_id=? AND message_id=?", + (chat_id, message_id), + ) + _conn.commit() + + +def record_download( + chat_id: str, + chat_title: str, + message_id: int, + file_name: str, + file_path: str, + file_size: int, + media_type: str, + status: str, +): + """写入或更新下载记录。同一 chat_id+message_id 重复插入时覆盖。""" + if _conn is None: + return + _conn.execute( + """ + INSERT INTO downloads + (chat_id, chat_title, message_id, file_name, file_path, file_size, media_type, + download_time, status) + VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now','localtime'), ?) + ON CONFLICT(chat_id, message_id) DO UPDATE SET + chat_title=excluded.chat_title, + file_name=excluded.file_name, + file_path=excluded.file_path, + file_size=excluded.file_size, + media_type=excluded.media_type, + download_time=excluded.download_time, + status=excluded.status + """, + (chat_id, chat_title, message_id, file_name, file_path, file_size, media_type, status), + ) + _conn.commit() + + +def query_records( + chat_id: str = "", + file_name: str = "", + status: str = "", + media_type: str = "", + date_from: str = "", + date_to: str = "", + limit: int = 50, + offset: int = 0, +) -> tuple: + """动态条件查询下载记录,返回 (records_list, total_count)。""" + if _conn is None: + return [], 0 + conditions = [] + params = [] + if chat_id: + conditions.append("(chat_id LIKE ? OR chat_title LIKE ?)") + params.extend([f"%{chat_id}%", f"%{chat_id}%"]) + if file_name: + conditions.append("file_name LIKE ?") + params.append(f"%{file_name}%") + if status: + conditions.append("status = ?") + params.append(status) + if media_type: + conditions.append("media_type = ?") + params.append(media_type) + if date_from: + conditions.append("download_time >= ?") + params.append(date_from) + if date_to: + conditions.append("download_time <= ?") + params.append(date_to + " 23:59:59") + where = ("WHERE " + " AND ".join(conditions)) if conditions else "" + total = _conn.execute( + f"SELECT COUNT(*) FROM downloads {where}", params + ).fetchone()[0] + rows = _conn.execute( + f"""SELECT id, chat_id, chat_title, message_id, file_name, file_path, + file_size, media_type, download_time, status + FROM downloads {where} ORDER BY id DESC LIMIT ? OFFSET ?""", + params + [limit, offset], + ).fetchall() + return [dict(row) for row in rows], total + + +def get_recent_history(limit: int = 50, offset: int = 0) -> list: + """返回最近的成功下载和手动跳过记录,供 Web UI 的已完成列表使用。""" + if _conn is None: + return [] + cur = _conn.execute( + """ + SELECT chat_id, chat_title, message_id, file_name, file_path, + file_size, media_type, download_time, status + FROM downloads + WHERE status IN ('success', 'skip') + ORDER BY id DESC + LIMIT ? OFFSET ? + """, + (limit, offset), + ) + return [dict(row) for row in cur.fetchall()] + + +def count_recent_history() -> int: + """返回已完成(成功+跳过)记录总数。""" + if _conn is None: + return 0 + return _conn.execute( + "SELECT COUNT(*) FROM downloads WHERE status IN ('success', 'skip')" + ).fetchone()[0] + + +# ──────────────────────────────────────────────────────────────── +# 预扫描结果缓存(给 banner「已完成 X / N」进度条用) +# ──────────────────────────────────────────────────────────────── + +def build_filter_key( + download_filter: Optional[str], + media_types: Optional[list], + file_formats: Optional[dict], +) -> str: + """根据过滤三元组构造稳定的缓存 key。 + 含动态时间关键字(now / today / date 函数)时返回 "__dynamic__",调用方应跳过缓存。 + """ + df = (download_filter or "").strip() + # 动态时间过滤器不缓存(每次跑都要重扫) + if df and re.search(r"\b(now|today|yesterday)\b", df, re.IGNORECASE): + return "__dynamic__" + + payload = json.dumps( + { + "f": df, + "m": sorted(media_types or []), + # 对每个 media 的 formats 也排序,保证顺序无关 + "ff": {k: sorted(v or []) for k, v in sorted((file_formats or {}).items())}, + }, + ensure_ascii=False, + sort_keys=True, + ) + return hashlib.sha1(payload.encode("utf-8")).hexdigest() + + +def get_scan_cache(chat_id: str, filter_key: str, ttl: int = 86400) -> Optional[int]: + """读取缓存的预计总数;超过 ttl 或不存在返回 None。""" + if _conn is None or filter_key == "__dynamic__": + return None + row = _conn.execute( + "SELECT estimated_total, scanned_at FROM scan_cache WHERE chat_id=? AND filter_key=?", + (chat_id, filter_key), + ).fetchone() + if row is None: + return None + if time.time() - row["scanned_at"] > ttl: + return None + return int(row["estimated_total"]) + + +def save_scan_cache(chat_id: str, filter_key: str, estimated_total: int): + """UPSERT 预扫描结果。""" + if _conn is None or filter_key == "__dynamic__": + return + _conn.execute( + """ + INSERT INTO scan_cache (chat_id, filter_key, estimated_total, scanned_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(chat_id, filter_key) DO UPDATE SET + estimated_total=excluded.estimated_total, + scanned_at=excluded.scanned_at + """, + (chat_id, filter_key, int(estimated_total), int(time.time())), + ) + _conn.commit() diff --git a/module/download_stat.py b/module/download_stat.py new file mode 100644 index 0000000..74acb5f --- /dev/null +++ b/module/download_stat.py @@ -0,0 +1,279 @@ +"""Download Stat""" +import asyncio +import time +from enum import Enum + +from pyrogram import Client + +from module.app import TaskNode + + +class DownloadState(Enum): + """Download state""" + + Downloading = 1 + StopDownload = 2 + + +_download_result: dict = {} +_paused_messages: set = set() # (chat_id, message_id) 单条暂停 +_skipped_messages: set = set() # (chat_id, message_id) 单条跳过 +_total_download_speed: int = 0 +_total_download_size: int = 0 +_last_download_time: float = time.time() +_download_state: DownloadState = DownloadState.Downloading + +# Task progress tracking +_task_progress: dict = { + "current_chat": "", + "current_chat_title": "", + "total_messages": 0, + "checked_messages": 0, + "skipped_files": 0, + "downloading_files": 0, + "completed_files": 0, + "failed_files": 0, + # 当次遍历已通过 filter 的消息条数(实时递增,无缓存时用它做分母) + "qualified_files": 0, + # 缓存命中后的预计下载总数;未命中时遍历结束后再赋值 + "estimated_total": 0, + "is_checking": False, + "last_update": 0, +} + + +def get_download_result() -> dict: + """get global download result""" + return _download_result + + +def get_total_download_speed() -> int: + """get total download speed""" + return _total_download_speed + + +def get_download_state() -> DownloadState: + """get download state""" + return _download_state + + +def get_task_progress() -> dict: + """get task progress with auto-detection of checking state""" + progress = _task_progress.copy() + + # Auto-detect if still checking based on last_update time + # If last update was within 3 seconds, consider it still active + if progress["current_chat"] and progress["last_update"] > 0: + time_since_update = time.time() - progress["last_update"] + # If there was recent activity (within 3 seconds), still checking + # If no recent activity and we have skipped files, checking is done + if time_since_update <= 3: + progress["is_checking"] = True + elif progress["skipped_files"] > 0 or progress["checked_messages"] > 0: + progress["is_checking"] = False + + return progress + + +def update_task_progress( + current_chat: str = None, + current_chat_title: str = None, + total_messages: int = None, + checked_messages: int = None, + skipped_files: int = None, + downloading_files: int = None, + completed_files: int = None, + failed_files: int = None, + qualified_files: int = None, + estimated_total: int = None, + is_checking: bool = None, +): + """update task progress""" + global _task_progress + if current_chat is not None: + _task_progress["current_chat"] = current_chat + if current_chat_title is not None: + _task_progress["current_chat_title"] = current_chat_title + if total_messages is not None: + _task_progress["total_messages"] = total_messages + if checked_messages is not None: + _task_progress["checked_messages"] = checked_messages + if skipped_files is not None: + _task_progress["skipped_files"] = skipped_files + if downloading_files is not None: + _task_progress["downloading_files"] = downloading_files + if completed_files is not None: + _task_progress["completed_files"] = completed_files + if failed_files is not None: + _task_progress["failed_files"] = failed_files + if qualified_files is not None: + _task_progress["qualified_files"] = qualified_files + if estimated_total is not None: + _task_progress["estimated_total"] = estimated_total + if is_checking is not None: + _task_progress["is_checking"] = is_checking + _task_progress["last_update"] = time.time() + + +def reset_task_progress(): + """reset task progress for new task""" + global _task_progress + _task_progress = { + "current_chat": "", + "current_chat_title": "", + "total_messages": 0, + "checked_messages": 0, + "skipped_files": 0, + "downloading_files": 0, + "completed_files": 0, + "failed_files": 0, + "qualified_files": 0, + "estimated_total": 0, + "is_checking": False, + "last_update": time.time(), + } + + +def increment_task_stat(stat_type: str, count: int = 1): + """increment a specific stat""" + global _task_progress + if stat_type in _task_progress and isinstance(_task_progress[stat_type], int): + _task_progress[stat_type] += count + _task_progress["last_update"] = time.time() + + +# pylint: disable = W0603 +def set_download_state(state: DownloadState): + """set download state""" + global _download_state + _download_state = state + + +def pause_message(chat_id: str, message_id: int): + """暂停单条消息下载""" + _paused_messages.add((chat_id, message_id)) + + +def resume_message(chat_id: str, message_id: int): + """继续单条消息下载""" + _paused_messages.discard((chat_id, message_id)) + + +def skip_message(chat_id: str, message_id: int): + """跳过单条消息下载""" + _skipped_messages.add((chat_id, message_id)) + _paused_messages.discard((chat_id, message_id)) + + +def is_message_paused(chat_id: str, message_id: int) -> bool: + return (chat_id, message_id) in _paused_messages + + +def is_message_skipped(chat_id: str, message_id: int) -> bool: + return (chat_id, message_id) in _skipped_messages + + +def clear_skipped_message(chat_id: str, message_id: int): + """清除跳过标记(下载流程退出时调用)""" + _skipped_messages.discard((chat_id, message_id)) + + +def remove_download_entry(chat_id, message_id): + """从下载结果中移除条目,使其不再显示在正在下载列表""" + chat_key = chat_id if not isinstance(chat_id, str) else chat_id + if chat_key in _download_result and message_id in _download_result[chat_key]: + del _download_result[chat_key][message_id] + + +async def update_download_status( + down_byte: int, + total_size: int, + message_id: int, + file_name: str, + start_time: float, + node: TaskNode, + client: Client, +): + """update_download_status""" + cur_time = time.time() + # pylint: disable = W0603 + global _total_download_speed + global _total_download_size + global _last_download_time + + if node.is_stop_transmission: + client.stop_transmission() + + chat_id = node.chat_id + _msg_key = (str(chat_id), message_id) + + # 单条跳过(不在此处 discard,让重试循环也能检测到) + if is_message_skipped(*_msg_key): + remove_download_entry(chat_id, message_id) + client.stop_transmission() + return + + # 单条暂停(阻塞直到继续或跳过) + while is_message_paused(*_msg_key): + if is_message_skipped(*_msg_key): + remove_download_entry(chat_id, message_id) + client.stop_transmission() + return + await asyncio.sleep(0.5) + + # 全局暂停 + while get_download_state() == DownloadState.StopDownload: + if node.is_stop_transmission: + client.stop_transmission() + await asyncio.sleep(1) + + if not _download_result.get(chat_id): + _download_result[chat_id] = {} + + if _download_result[chat_id].get(message_id): + last_download_byte = _download_result[chat_id][message_id]["down_byte"] + last_time = _download_result[chat_id][message_id]["end_time"] + download_speed = _download_result[chat_id][message_id]["download_speed"] + each_second_total_download = _download_result[chat_id][message_id][ + "each_second_total_download" + ] + end_time = _download_result[chat_id][message_id]["end_time"] + + _total_download_size += down_byte - last_download_byte + each_second_total_download += down_byte - last_download_byte + + if cur_time - last_time >= 1.0: + download_speed = int(each_second_total_download / (cur_time - last_time)) + end_time = cur_time + each_second_total_download = 0 + + download_speed = max(download_speed, 0) + + _download_result[chat_id][message_id]["down_byte"] = down_byte + _download_result[chat_id][message_id]["end_time"] = end_time + _download_result[chat_id][message_id]["download_speed"] = download_speed + _download_result[chat_id][message_id][ + "each_second_total_download" + ] = each_second_total_download + else: + each_second_total_download = down_byte + _download_result[chat_id][message_id] = { + "down_byte": down_byte, + "total_size": total_size, + "file_name": file_name, + "start_time": start_time, + "end_time": cur_time, + "download_speed": down_byte / (cur_time - start_time), + "each_second_total_download": each_second_total_download, + "task_id": node.task_id, + } + _total_download_size += down_byte + + if cur_time - _last_download_time >= 1.0: + # update speed + _total_download_speed = int( + _total_download_size / (cur_time - _last_download_time) + ) + _total_download_speed = max(_total_download_speed, 0) + _total_download_size = 0 + _last_download_time = cur_time diff --git a/module/filter.py b/module/filter.py new file mode 100644 index 0000000..ea589c0 --- /dev/null +++ b/module/filter.py @@ -0,0 +1,404 @@ +"""Filter for download""" + +import re +from datetime import datetime +from typing import Any, Optional, Tuple + +from ply import lex, yacc + +from utils.format import get_byte_from_str +from utils.meta_data import MetaData, NoneObj, ReString + + +# pylint: disable = R0904 +class BaseFilter: + """for normal filter""" + + def __init__(self, debug: bool = False): + """ + Parameters + ---------- + debug: bool + If output debug info + + """ + self.names: dict = {} + self.debug = debug + # Build the lexer and parser + # lex.lex(module=self) + self.lexer = lex.lex(module=self) + self.yacc = yacc.yacc(module=self) + + def reset(self): + """Reset all symbol""" + self.names.clear() + + def exec(self, filter_str: str) -> Any: + """Exec filter str""" + # ) # + # return yacc.parse(filter_str, debug=self.debug) + return self.yacc.parse(filter_str, debug=self.debug) + + def _output(self, output_str: str): + """For print debug info""" + if self.debug: + print(output_str) + + reserved = { + "and": "AND", + "or": "OR", + "not": "NOT", + "matches": "MATCHES", + "contains": "CONTAINS", + } + + tokens = ( + "NAME", + "NUMBER", + "GE", + "LE", + "LOR", + "LAND", + "STRING", + "RESTRING", + "BYTE", + "EQ", + "NE", + "TIME", + "AND", + "OR", + "NOT", + "MATCHES", + "CONTAINS", + ) + + literals = ["=", "+", "-", "*", "/", "(", ")", ">", "<"] + + # t_NAME = r'[a-zA-Z_][a-zA-Z0-9_]*' + t_GE = r">=" + t_LE = r"<=" + t_LOR = r"\|\|" + t_LAND = r"&&" + t_EQ = r"==" + t_NE = r"!=" + + def t_BYTE(self, t): + r"\d{1,}(B|KB|MB|GB|TB)" + t.value = get_byte_from_str(t.value) + t.type = "NUMBER" + return t + + def t_TIME(self, t): + r"\d{4}-\d{1,2}-\d{1,2}[ ]{1,}\d{1,2}:\d{1,2}:\d{1,2}" + t.value = datetime.strptime(t.value, "%Y-%m-%d %H:%M:%S") + return t + + def t_STRING(self, t): + r"'.*?'" + # r"'([^\\']+|\\'|\\\\)*'" + t.value = t.value[1:-1] + return t + + def t_RESTRING(self, t): + r"r'.*?'" + # r"r'([^\\']+|\\'|\\\\)*'" + t.value = t.value[2:-1] + return t + + def t_NAME(self, t): + r"[a-zA-Z_][a-zA-Z0-9_]*" + t.type = BaseFilter.reserved.get(t.value, "NAME") + return t + + def t_NUMBER(self, t): + r"\d+" + t.value = int(t.value) + return t + + t_ignore = " \t" + + def t_newline(self, t): + r"\n+" + t.lexer.lineno += t.value.count("\n") + + def t_error(self, t): + """print error""" + print(f"Illegal character '{t.value[0]}'") + t.lexer.skip(1) + + precedence = ( + ("left", "LOR", "OR"), + ("left", "LAND", "AND"), + ("right", "NOT"), + ("left", "EQ", "NE"), + ("nonassoc", ">", "<", "GE", "LE"), + ("nonassoc", "MATCHES", "CONTAINS"), + ("left", "+", "-"), + ("left", "*", "/"), + ("right", "UMINUS"), + ) + + def p_statement_assign(self, p): + 'statement : NAME "=" expression' + return self.p_expression_eq(p) + # self.names[p[1]] = p[3] + + def p_statement_expr(self, p): + "statement : expression" + self._output(p[1]) + p[0] = p[1] + + def p_expression_binop(self, p): + """expression : expression '+' expression + | expression '-' expression + | expression '*' expression + | expression '/' expression""" + self.check_type(p) + if isinstance(p[1], NoneObj): + p[1] = 0 + if isinstance(p[3], NoneObj): + p[3] = 0 + + if p[2] == "+": + p[0] = p[1] + p[3] + elif p[2] == "-": + p[0] = p[1] - p[3] + elif p[2] == "*": + p[0] = p[1] * p[3] + elif p[2] == "/": + p[0] = p[1] / p[3] + + self._output(f"binop {p[1]} {p[2]} {p[3]} = {p[0]}") + + def p_expression_comp(self, p): + """expression : expression '>' expression + | expression '<' expression""" + self.check_type(p) + if isinstance(p[1], NoneObj) or isinstance(p[3], NoneObj): + p[0] = True + return + + if p[1] is None or p[3] is None: + p[0] = False + return + if p[2] == ">": + p[0] = p[1] > p[3] + elif p[2] == "<": + p[0] = p[1] < p[3] + + def p_expression_uminus(self, p): + "expression : '-' expression %prec UMINUS" + p[0] = -p[2] + + def p_expression_ge(self, p): + "expression : expression GE expression" + self.check_type(p) + if isinstance(p[1], NoneObj) or isinstance(p[3], NoneObj): + p[0] = True + return + + if p[1] is None or p[3] is None: + p[0] = False + return + + p[0] = p[1] >= p[3] + self._output(f"{p[1]} {p[2]} {p[3]} {p[0]}") + + def p_expression_le(self, p): + "expression : expression LE expression" + self.check_type(p) + if isinstance(p[1], NoneObj) or isinstance(p[3], NoneObj): + p[0] = True + return + + if p[1] is None or p[3] is None: + p[0] = False + return + + p[0] = p[1] <= p[3] + self._output(f"{p[1]} {p[2]} {p[3]} = {p[0]}") + + def p_expression_eq(self, p): + "expression : expression EQ expression" + self.check_type(p) + if isinstance(p[1], NoneObj) or isinstance(p[3], NoneObj): + p[0] = True + return + + if p[1] is None or p[3] is None: + p[0] = False + return + + if isinstance(p[3], ReString): + if not isinstance(p[1], str): + p[0] = 0 + return + p[0] = re.fullmatch(p[3].re_string, p[1], re.MULTILINE) is not None + self._output(f"{p[1]} {p[2]} {p[3].re_string} {p[0]}") + elif isinstance(p[1], ReString): + if not isinstance(p[3], str): + p[0] = 0 + return + p[0] = re.fullmatch(p[1].re_string, p[3], re.MULTILINE) is not None + self._output(f"{p[1]} {p[2]} {p[3].re_string} {p[0]}") + else: + p[0] = p[1] == p[3] + self._output(f"{p[1]} {p[2]} {p[3]} {p[0]}") + + def p_expression_ne(self, p): + "expression : expression NE expression" + self.check_type(p) + if isinstance(p[1], NoneObj) or isinstance(p[3], NoneObj): + p[0] = True + return + + if p[1] is None or p[3] is None: + p[0] = False + return + if isinstance(p[3], ReString): + if not isinstance(p[1], str): + p[0] = 0 + return + p[0] = re.fullmatch(p[3].re_string, p[1], re.MULTILINE) is None + self._output(f"{p[1]} {p[2]} {p[3].re_string} {p[0]}") + elif isinstance(p[1], ReString): + if not isinstance(p[3], str): + p[0] = 0 + return + p[0] = re.fullmatch(p[1].re_string, p[3], re.MULTILINE) is None + self._output(f"{p[1]} {p[2]} {p[3].re_string} {p[0]}") + else: + p[0] = p[1] != p[3] + self._output(f"{p[1]} {p[2]} {p[3]} = {p[0]}") + + def p_expression_group(self, p): + "expression : '(' expression ')'" + p[0] = p[2] + + def p_expression_number(self, p): + "expression : NUMBER" + p[0] = p[1] + + def p_expression_time(self, p): + "expression : TIME" + p[0] = p[1] + + def p_expression_byte(self, p): + "expression : BYTE" + p[0] = p[1] + + def p_expression_name(self, p): + "expression : NAME" + try: + p[0] = self.names[p[1]] + except Exception as e: + self._output(f"Undefined name '{p[1]}'") + raise ValueError(f"Undefined name {p[1]}") from e + # FIXME: not support not exist name + # p[0] = NoneObj() + + def p_expression_lor(self, p): + "expression : expression LOR expression" + p[0] = p[1] or p[3] + + def p_expression_land(self, p): + "expression : expression LAND expression" + p[0] = p[1] and p[3] + + def p_expression_or(self, p): + "expression : expression OR expression" + p[0] = p[1] or p[3] + + def p_expression_and(self, p): + "expression : expression AND expression" + p[0] = p[1] and p[3] + + def p_expression_string(self, p): + "expression : STRING" + p[0] = p[1] + + def p_expression_restring(self, p): + "expression : RESTRING" + p[0] = ReString(p[1]) + self._output("RESTRING : " + p[0].re_string) + + def p_expression_not(self, p): + "expression : NOT expression" + if isinstance(p[2], NoneObj) or p[2] is None: + p[0] = True + return + p[0] = not bool(p[2]) + + def p_expression_matches(self, p): + "expression : expression MATCHES STRING" + if isinstance(p[1], NoneObj) or p[1] is None: + p[0] = True + return + try: + p[0] = bool(re.search(p[3], str(p[1]))) + except re.error: + p[0] = False + + def p_expression_contains(self, p): + "expression : expression CONTAINS STRING" + if isinstance(p[1], NoneObj) or p[1] is None: + p[0] = True + return + p[0] = p[3].lower() in str(p[1]).lower() + + # pylint: disable = C0116 + def p_error(self, p): + if p: + raise ValueError(f"Syntax error at '{p.value}'") + + raise ValueError("Syntax error at EOF") + + def check_type(self, p): + """Check filter type if is right""" + if p[1] is None or p[1] is NoneObj or p[3] is None or p[3] is NoneObj: + return + if isinstance(p[1], str): + if not isinstance(p[3], str) and not isinstance(p[3], ReString): + raise ValueError(f"{p[1]} is str but {p[3]} is not") + elif isinstance(p[1], int): + if not isinstance(p[3], int): + raise ValueError(f"{p[1]} is int but {p[3]} is not") + elif isinstance(p[1], bool): + if not isinstance(p[3], bool): + raise ValueError(f"{p[1]} is bool but {p[3]} is not") + elif isinstance(p[1], datetime): + if not isinstance(p[3], datetime): + raise ValueError(f"{p[1]} is datetime but {p[3]} is not") + + +class Filter: + """filter for telegram download""" + + def __init__(self): + self.filter = BaseFilter() + + def set_meta_data(self, meta_data: MetaData): + """Set meta data for filter""" + self.filter.reset() + self.filter.names = meta_data.data() + + def set_debug(self, debug: bool): + """Set Filter Debug Model""" + self.filter.debug = debug + + def exec(self, filter_str: str) -> bool: + """Exec filter str""" + + if self.filter.names: + res = self.filter.exec(filter_str) + if isinstance(res, bool): + return res + return False + raise ValueError("meta data cannot be empty!") + + def check_filter(self, filter_str: str) -> Tuple[bool, Optional[str]]: + """check filter str""" + try: + return not self.exec(filter_str) is None, None + except Exception as e: + return False, str(e) diff --git a/module/get_chat_history_v2.py b/module/get_chat_history_v2.py new file mode 100644 index 0000000..7d3f2f8 --- /dev/null +++ b/module/get_chat_history_v2.py @@ -0,0 +1,98 @@ +"""Rewrite pyrogram.get_chat_history""" + +from datetime import datetime +from typing import AsyncGenerator, Optional, Union + +import pyrogram + +# pylint: disable = W0611 +from pyrogram import raw, types, utils + + +async def get_chunk_v2( + *, + client: pyrogram.Client, + chat_id: Union[int, str], + limit: int = 0, + offset: int = 0, + max_id: int = 0, + from_message_id: int = 0, + from_date: datetime = utils.zero_datetime(), + reverse: bool = False +): + """get chunk""" + from_message_id = from_message_id or (1 if reverse else 0) + + messages = await utils.parse_messages( + client, + await client.invoke( + raw.functions.messages.GetHistory( + peer=await client.resolve_peer(chat_id), + offset_id=from_message_id, + offset_date=utils.datetime_to_timestamp(from_date), + add_offset=offset * (-1 if reverse else 1) - (limit if reverse else 0), + limit=limit, + max_id=max_id, + min_id=0, + hash=0, + ), + sleep_threshold=60, + ), + replies=0, + ) + + if reverse: + messages.reverse() + + return messages + + +# pylint: disable = C0301 +async def get_chat_history_v2( + self: pyrogram.Client, + chat_id: Union[int, str], + limit: int = 0, + max_id: int = 0, + offset: int = 0, + offset_id: int = 0, + offset_date: datetime = utils.zero_datetime(), + reverse: bool = False, +) -> Optional[AsyncGenerator["types.Message", None]]: + """Get messages from a chat history.""" + current = 0 + total = limit or (1 << 31) - 1 + limit = min(100, total) + + while True: + messages = await get_chunk_v2( + client=self, + chat_id=chat_id, + limit=limit, + offset=offset, + max_id=max_id + 1 if max_id else 0, + from_message_id=offset_id, + from_date=offset_date, + reverse=reverse, + ) + + if not messages: + break_count = offset_id - 1 + async for message in self.get_chat_history(chat_id): + if break_count: + break_count -= 1 + continue + if len(messages) >= limit + 1: + break + messages.append(message) + if not messages: + return + + offset_id = messages[-1].id + (1 if reverse else 0) + + for message in messages: + yield message + + current += 1 + + if current >= total: + return diff --git a/module/language.py b/module/language.py new file mode 100644 index 0000000..292e2e2 --- /dev/null +++ b/module/language.py @@ -0,0 +1,284 @@ +"""Multi language support""" + +# disable pylint: disable = C0301 +from enum import Enum + + +class Language(Enum): + """Language for ui""" + + EN = 1 # english + ZH = 2 # china + RU = 3 # russian + UA = 4 # ukrainian + + +_language = Language.EN + + +def set_language(language: Language): + """Set Lanaguage""" + # pylint: disable = W0603 + global _language + _language = language + + +translations = { + "Forward": ["转发", "Переслать", "Переслати"], + "Total": ["总数", "Всего", "Всього"], + "Success": ["成功", "Успешно", "Успішно"], + "Failed": ["失败", "Не удалось", "Не вдалося"], + "Skipped": ["跳过", "Пропущено", "Пропущено"], + "Message ID": ["消息ID", "ID сообщения", "ID повідомлення"], + "Telegram Media Downloader": [ + "电报媒体下载器", + "Telegram Media Downloader", + "Telegram Media Downloader", + ], + "Version": ["版本", "Версия", "Версія"], + "Downloading": ["下载", "Скачивание", "Скачування"], + "Available commands:": ["可用命令:", "Доступные команды:", "Доступні команди:"], + "Show available commands": [ + "显示可用命令", + "Показать доступные команды", + "Показати доступні команди", + ], + "Download messages": ["下载消息", "Скачать сообщения", "Скачати повідомлення"], + "Forward messages": ["转发消息", "Переслать сообщения", "Переслати повідомлення"], + "Listen for forwarded messages": [ + "监听转发消息", + "Прослушивать пересланные сообщения", + "Прослуховувати переслані повідомлення", + ], + "Set language": ["设置语言", "Установить язык", "Встановити мову"], + "**Note**: 1 means the start of the entire chat": [ + "**注意**: 1表示整个聊天的开始", + "**Примечание**: 1 означает начало всего чата", + "**Увага**: 1 означає початок всього чату", + ], + "0 means the end of the entire chat": [ + "0表示整个聊天的结束", + "0 означает конец всего чата", + "0 означає кінець всього чату", + ], + "means optional, not required": [ + "表示可选项,非必填", + "означает необязательный параметр", + "означає необов'язковий параметр", + ], + "To download the video, use the method to directly enter /download to view": [ + "下载视频,使用方法直接输入/download查看", + "Чтобы скачать видео, введите /download для просмотра", + "Щоб скачати відео, введіть /download для перегляду", + ], + "Forward video, use the method to directly enter /forward to view": [ + "转发视频,使用方法直接输入/forward查看", + "Переслать видео, введите /forward для просмотра", + "Переслати відео, введіть /forward для перегляду", + ], + "Listen forward, use the method to directly enter /listen_forward to view": [ + "监控转发,使用方法直接输入/listen_forward查看", + "Слушать пересылку, введите /listen_forward для просмотра", + "Слухати пересилання, введіть /listen_forward для перегляду", + ], + "Add download filter, use the method to directly enter /add_filter to view": [ + "添加下载过滤器", + "Добавить фильтр загрузки, используйте метод, чтобы непосредственно ввести /add_filter для просмотра", + "Додати фільтр завантаження, використовуйте метод, щоб безпосередньо ввести /add_filter для перегляду", + ], + "Help": ["帮助", "Помощь", "Допомога"], + "Invalid command format": [ + "无效的命令格式", + "Неверный формат команды", + "Невірний формат команди", + ], + "Invalid command format. Please use /set_language en/ru/zh/ua": [ + "无效的命令格式。请使用 /set_language en/ru/zh/ua", + "Неверный формат команды. Пожалуйста, используйте /set_language en/ru/zh/ua", + "Невірний формат команди. Будь ласка, використовуйте /set_language en/ru/zh/ua", + ], + "Language set to English": [ + "语言设置为中文", + "Выбран английский язык", + "Обрано англійську мову", + ], + "Language set to": [ + "语言设置为", + "Выбран язык", + "Обрано мову", + ], + "Invalid command format. Please use /add_filter your filter": [ + "无效的命令格式。请使用 /add_filter 你的过滤规则", + "Неверный формат команды. Пожалуйста, используйте /add_filter ВашФильтр", + "Невірний формат команди. Будь ласка, використовуйте /add_filter ВашФільтр", + ], + "Add download filter": [ + "添加下载过滤器", + "Добавить фильтр скачивания", + "Додати фільтр скачування", + ], + "Check error, please add again": [ + "检验错误,请重新添加", + "Ошибка проверки, пожалуйста, добавьте еще раз", + "Помилка перевірки, будь ласка, додайте ще раз", + ], + "Direct download, directly forward the message to your robot": [ + "直接下载,直接转发消息给你的机器人", + "Скачивание напрямую, пересылка сообщения напрямую вашему роботу", + "Безпосереднє скачування, безспесередня пересилка повідомлення вашому роботу", + ], + "Directly download a single message": [ + "直接下载单条消息", + "Прямое скачивание одного сообщения", + "Безпосереднє скачування одного повідомлення", + ], + "From": ["从", "От", "Від"], + "download": ["下载", "скачать", "скачати"], + "error": ["错误", "ошибка", "помилка"], + "Parameter error, please enter according to the reference format": [ + "参数错误,请根据参考格式输入", + "Ошибка параметра, введите в соответствии с форматом ссылки", + "Помилка параметра, введіть відповідно до формату посилання", + ], + "Download all messages of common group": [ + "下载公共群组的所有消息", + "Скачать все сообщения общей группы", + "Скачати всі повідомлення спільної групи", + ], + "The private group (channel) link is a random group message link": [ + "私密群组(频道) 链接为随便复制一条群组消息链接", + "Ссылка на частную группу (канал) - это ссылка на случайное сообщение группы", + "Посилання на приватну групу (канал) - це посилання на випадкове повідомлення групи", + ], + "The download starts from the N message to the end of the M message": [ + "下载从第N条消息开始的到第M条信息结束", + "Скачивание начинается с сообщения N до конца сообщения M", + "Скачування починається з повідомлення N до кінця повідомлення M", + ], + "When M is 0, it means the last message. The filter is optional": [ + "M为0的时候表示到最后一条信息,过滤器为可选", + "Когда M равно 0, это означает последнее сообщение. Фильтр необязателен", + "Коли M дорівнює 0, це означає останнє повідомлення. Фільтр необов'язковий", + ], + "chat input error, please enter the channel or group link": [ + "chat输入错误,请输入频道或群组的链接", + "Ошибка ввода чата, введите ссылку на канал или группу", + "Помилка введеня чату, введіть посилання на канал або групу", + ], + "Error type": ["错误类型", "Тип ошибки", "Тип помилки"], + "Exception message": ["异常消息", "Сообщение исключения", "Повідомлення винятка"], + "Invalid chat link": [ + "无效的聊天链接", + "Ошибочная ссылка на чат", + "Помилкове посилання на чат", + ], + "Cannot be forwarded to this bot, will cause an infinite loop": [ + "不能转发给该机器人,会导致无限循环", + "Невозможно переслать этому боту, это вызовет бесконечный цикл", + "Неможливо переслати цьому боту, це спричинить безкінечний цикл", + ], + "Please use": ["请使用", "Пожалуйста, используйте", "Будь ласка, використовуйте"], + "Filter": ["过滤器", "Фильтр", "Фільтр"], + "Error forwarding message": [ + "失败的转发消息", + "Ошибка пересылки сообщения", + "Помилка пересилки повідомлення", + ], + "file reference expired, refetching": [ + "文件引用过期,重新获取中", + "Ссылка на файл истекла, повторное получение", + "Посилання на файл минуло, повторне отримання", + ], + "file reference expired for 3 retries, download skipped": [ + "文件引用过期重试超过3次,跳过下载", + "Ссылка на файл истекла после 3 попыток, загрузка пропущена", + "Посилання на файл минуло після 3 спроб, завантаження пропущено", + ], + "Timeout Error occurred when downloading Message": [ + "下载消息超时错误", + "Ошибка времени ожидания при скачивании сообщения", + "Помилка часу очікування при скачуванні повідомлення", + ], + "retrying": ["重试", "повторная попытка", "повторна спроба"], + "seconds": ["秒", "секунд", "секунд"], + "Timing out after 3 reties, download skipped": [ + "超时重试超过3次,跳过下载", + "Истекло время ожидания после 3 попыток, загрузка пропущена", + "Час очікування закінчився після 3 спроб, завантаження пропущено", + ], + "could not be downloaded due to following exception": [ + "无法下载,因为以下异常", + "не может быть скачен по следующей причине", + "не може бути скачаний з наступної причини", + ], + "Downloading files failed during last run": [ + "下载最后一次运行失败的文件", + "Скачивание файлов не удалось во время последнего запуска", + "Скачування файлів не вдалося під час останнього запуску", + ], + "Successfully started (Press Ctrl+C to stop)": [ + "成功启动(按Ctrl+C停止)", + "Запуск успешный (нажмите Ctrl + C для остановки)", + "Запуск успішний (натисніть Ctrl + C для зупинки)", + ], + "KeyboardInterrupt": ["键盘中断", "KeyboardInterrupt", "KeyboardInterrupt"], + "update config": ["更新配置", "обновить конфигурацию", "оновити конфігурацію"], + "Updated last read message_id to config file": [ + "更新最后阅读消息ID到配置文件", + "Обновлен идентификатор последнего прочитанного сообщения в конфигурационном файле", + "Оновлено ідентифікатор останнього прочитаного повідомлення у конфігураційному файлі", + ], + "total download": ["总下载", "всего скачено", "всього скачано"], + "total upload file": ["总上传文件", "всего скаченных файлов", "всього скачаних файлів"], + "Stopped": ["停止", "остановлено", "зупинено"], + "already download,download skipped": [ + "已下载,已跳过下载", + "уже скачен, скачивание пропущена", + "вже скачан, скачування пропущено", + ], + "Media downloaded with wrong size": [ + "媒体下载错误的大小", + "Медиафайл скачен с неправильным размером", + "Медіафайл скачано з неправильним розміром", + ], + "actual": ["实际", "фактический", "фактичний"], + "file name": ["文件名", "имя файла", "ім'я файлу"], + "Successfully downloaded": ["成功下载", "Успешно скачано", "Успішно скачано"], + "Get group and user info from message link": [ + "从消息链接中获取群组和用户信息", + "Получить информацию о группе и пользователе по ссылке на сообщение", + "Отримайте інформацію про групу та користувача за посиланням у повідомленні", + ], + "Upload Progresses": ["上传进度", "Прогресс загрузки", "Прогрес завантаження"], + "Download Progresses": ["下载进度", "Прогресс скачивания", "Прогрес завантаження"], + "New Version": ["新版本", "новая версия", "нова версія"], + "Stop bot download or forward": [ + "停止机器人下载或转发", + "Остановить загрузку или пересылку ботом", + "Зупинити завантаження або пересилання ботом", + ], + "Forward a specific media to a comment section": [ + "将特定媒体转发至评论", + "Переслать определенное медиа в комментарии", + "Переслати конкретне медіа в коментарі", + ], +} + + +def _t(text: str): + """Get translation + Parameters + ---------- + text : str + language : str + Returns + ------- + str + """ + if _language is Language.EN: + return text + + if text in translations: + return translations[text][_language.value - 2] + + return text diff --git a/module/pyrogram_extension.py b/module/pyrogram_extension.py new file mode 100644 index 0000000..c698d0f --- /dev/null +++ b/module/pyrogram_extension.py @@ -0,0 +1,1339 @@ +"""Pyrogram ext""" + +import asyncio +import os +import secrets +import struct +import time +from datetime import datetime +from functools import wraps +from io import BytesIO, StringIO +from mimetypes import MimeTypes +from typing import Callable, Iterable, List, Optional, Union + +import pyrogram +from loguru import logger +from pyrogram import types +from pyrogram.client import Cache +from pyrogram.file_id import ( + FILE_REFERENCE_FLAG, + PHOTO_TYPES, + WEB_LOCATION_FLAG, + FileType, + b64_decode, + rle_decode, +) +from pyrogram.mime_types import mime_types + +from module.app import ( + Application, + CloudDriveUploadStat, + DownloadStatus, + ForwardStatus, + TaskNode, + UploadProgressStat, + UploadStatus, +) +from module.download_stat import get_download_result +from module.language import Language, _t +from module.send_media_group_v2 import cache_media, send_media_group_v2 +from utils.format import ( + create_progress_bar, + extract_info_from_link, + format_byte, + truncate_filename, +) +from utils.meta_data import MetaData + +_mimetypes = MimeTypes() +_mimetypes.readfp(StringIO(mime_types)) +_download_cache = Cache(1024 * 1024 * 1024) + + +def reset_download_cache(): + """Reset download cache""" + _download_cache.store.clear() + + +def _guess_mime_type(filename: str) -> Optional[str]: + """Guess mime type""" + return _mimetypes.guess_type(filename)[0] + + +def _guess_extension(mime_type: str) -> Optional[str]: + """Guess extension""" + return _mimetypes.guess_extension(mime_type) + + +def get_media_obj( + message: pyrogram.types.Message, media: str = None, caption: str = None +) -> Union[ + types.InputMediaPhoto, + types.InputMediaVideo, + types.InputMediaAudio, + types.InputMediaDocument, + types.InputMediaAnimation, +]: + """Get media object""" + media_type = message.media + if media_type == pyrogram.enums.MessageMediaType.PHOTO: + return types.InputMediaPhoto(media, caption=caption) + + if media_type == pyrogram.enums.MessageMediaType.VIDEO: + return types.InputMediaVideo( + media, + caption=caption, + width=message.video.width, + height=message.video.height, + duration=message.video.duration, + ) + + if media_type in [ + pyrogram.enums.MessageMediaType.AUDIO, + pyrogram.enums.MessageMediaType.VOICE, + ]: + return types.InputMediaAudio(media, caption=caption) + + if media_type == pyrogram.enums.MessageMediaType.DOCUMENT: + return types.InputMediaDocument(media, caption=caption) + + if media_type == pyrogram.enums.MessageMediaType.ANIMATION: + return types.InputMediaAnimation(media, caption=caption) + + return None + + +def _get_file_type(file_id: str): + """Get file type""" + decoded = rle_decode(b64_decode(file_id)) + + # File id versioning. Major versions lower than 4 don't have a minor version + major = decoded[-1] + + if major < 4: + buffer = BytesIO(decoded[:-1]) + else: + buffer = BytesIO(decoded[:-2]) + + file_type, _ = struct.unpack(" str: + """Get extension""" + + if not file_id: + if dot: + return ".unknown" + return "unknown" + + file_type = _get_file_type(file_id) + + guessed_extension = _guess_extension(mime_type) + + if file_type in PHOTO_TYPES: + extension = "jpg" + elif file_type == FileType.VOICE: + extension = guessed_extension or "ogg" + elif file_type in (FileType.VIDEO, FileType.ANIMATION, FileType.VIDEO_NOTE): + extension = guessed_extension or "mp4" + elif file_type == FileType.DOCUMENT: + extension = guessed_extension or "zip" + elif file_type == FileType.STICKER: + extension = guessed_extension or "webp" + elif file_type == FileType.AUDIO: + extension = guessed_extension or "mp3" + else: + extension = "unknown" + + if dot: + extension = "." + extension + return extension + + +async def send_message_by_language( + client: pyrogram.client.Client, + language: Language, + chat_id: Union[int, str], + reply_to_message_id: int, + language_str: List[str], +): + """Record download status""" + msg = language_str[language.value - 1] + + return await client.send_message( + chat_id, msg, reply_to_message_id=reply_to_message_id + ) + + +async def download_thumbnail( + client: pyrogram.Client, + temp_path: str, + message: pyrogram.types.Message, +): + """Downloads the thumbnail of a video message to a temporary file. + + Args: + client: A Pyrogram client instance. + temp_path: The path to a temporary directory where the thumbnail file + will be stored. + message: A Pyrogram Message object representing the video message. + + Returns: + A string representing the path of the thumbnail file, or None if the + download failed. + + Raises: + ValueError: If the downloaded thumbnail file size doesn't match the + expected file size. + """ + thumbnail_file = None + if message.video.thumbs: + message = await fetch_message(client, message) + thumbnail = message.video.thumbs[0] if message.video.thumbs else None + unique_name = os.path.join( + temp_path, + "thumbnail", + f"thumb-{int(time.time())}-{secrets.token_hex(8)}.jpg", + ) + + max_attempts = 3 + for attempt in range(1, max_attempts + 1): + try: + thumbnail_file = await client.download_media( + thumbnail, file_name=unique_name + ) + + if os.path.getsize(thumbnail_file) == thumbnail.file_size: + break + + raise ValueError( + f"Thumbnail file size is {os.path.getsize(thumbnail_file)}" + f" bytes, actual {thumbnail.file_size}: {thumbnail_file}" + ) + + except Exception as e: + if attempt == max_attempts: + logger.exception( + f"Failed to download thumbnail after {max_attempts}" + f" attempts: {e}" + ) + else: + message = await fetch_message(client, message) + logger.warning( + f"Attempt {attempt} to download thumbnail failed: {e}" + ) + # Wait 2 seconds before retrying + await asyncio.sleep(2) + + thumbnail = None + thumbnail_file = None + return thumbnail_file + + +async def upload_telegram_chat( + client: pyrogram.Client, + upload_user: pyrogram.Client, + app: Application, + node: TaskNode, + message: pyrogram.types.Message, + download_status: DownloadStatus, + file_name: str = None, +): + """Upload telegram chat""" + # upload telegram + if node.upload_telegram_chat_id: + if download_status is DownloadStatus.SkipDownload and message.media: + if message.media_group_id: + await proc_cache_forward(client, node, message, True) + return + + if download_status is DownloadStatus.SuccessDownload or ( + download_status is DownloadStatus.SkipDownload and not message.media + ): + try: + await upload_telegram_chat_message( + client, + upload_user, + app, + node, + message, + file_name, + ) + except Exception as e: + logger.exception(f"Upload file {file_name} error: {e}") + finally: + if file_name and app.after_upload_telegram_delete: + os.remove(file_name) + + # forward text + # FIXME: fix upload text + # if ( + # download_status is DownloadStatus.SkipDownload + # and message.text + # and bot + # ): + # await upload_telegram_chat( + # client, app, node.upload_telegram_chat_id, message, file_name + # ) + + +async def upload_telegram_chat_message( + client: pyrogram.Client, + upload_user: pyrogram.Client, + app: Application, + node: TaskNode, + message: pyrogram.types.Message, + file_name: str = None, +) -> ForwardStatus: + """See upload telegram_chat""" + forward_status = ForwardStatus.FailedForward + max_attempts = 3 + for _ in range(1, max_attempts + 1): + try: + forward_status = await _upload_telegram_chat_message( + client, upload_user, app, node, message, file_name + ) + break + except pyrogram.errors.exceptions.flood_420.FloodWait as wait_err: + await asyncio.sleep(wait_err.value * 2) + logger.warning( + "Upload Message[{}]: FlowWait {}", message.id, wait_err.value + ) + except Exception as e: + logger.exception(f"Upload file {file_name} error: {e}") + return ForwardStatus.FailedForward + + if forward_status != ForwardStatus.CacheForward: + node.stat_forward(forward_status) + return forward_status + + +# pylint: disable=R0912 +async def _upload_signal_message( + client: pyrogram.Client, + upload_user: pyrogram.Client, + app: Application, + node: TaskNode, + upload_telegram_chat_id: Union[int, str, None], + message: pyrogram.types.Message, + file_name: Optional[str], + caption: Optional[str] = None, +): + """ + Uploads a video or message to a Telegram chat. + + Parameters: + client (pyrogram.Client): The pyrogram client. + upload_telegram_chat_id (Union[int, str]): The ID of the chat to upload to. + message (pyrogram.types.Message): The message to upload. + file_name (str): The name of the file to upload. + """ + ui_file_name = file_name + if file_name: + ui_file_name = ( + f"****{os.path.splitext(file_name)[-1]}" + if app.hide_file_name + else file_name + ) + + if message.video: + # Download thumbnail + thumbnail_file = await download_thumbnail(client, app.temp_save_path, message) + try: + # TODO(tangyoha): add more log when upload video more than 2000MB failed + # Send video to the destination chat + if node.reply_to_message: + await node.reply_to_message.reply_video( + file_name, + caption=caption, + message_thread_id=node.topic_id, + thumb=thumbnail_file, + width=message.video.width, + height=message.video.height, + duration=message.video.duration, + parse_mode=pyrogram.enums.ParseMode.HTML, + ) + else: + await upload_user.send_video( + upload_telegram_chat_id, + file_name, + thumb=thumbnail_file, + width=message.video.width, + height=message.video.height, + duration=message.video.duration, + caption=caption, + parse_mode=pyrogram.enums.ParseMode.HTML, + progress=update_upload_stat, + progress_args=( + message.id, + ui_file_name, + time.time(), + node, + upload_user, + ), + message_thread_id=node.topic_id, + ) + except Exception as e: + raise e + finally: + if thumbnail_file: + os.remove(str(thumbnail_file)) + + elif message.photo: + if node.reply_to_message: + await node.reply_to_message.reply_photo( + file_name, + caption=caption, + message_thread_id=node.topic_id, + ) + else: + await upload_user.send_photo( + upload_telegram_chat_id, + file_name, + caption=caption, + progress=update_upload_stat, + progress_args=( + message.id, + ui_file_name, + time.time(), + node, + upload_user, + ), + message_thread_id=node.topic_id, + ) + elif message.document: + if node.reply_to_message: + await node.reply_to_message.reply_document( + file_name, + caption=caption, + message_thread_id=node.topic_id, + ) + else: + await upload_user.send_document( + upload_telegram_chat_id, + file_name, + caption=caption, + progress=update_upload_stat, + progress_args=( + message.id, + ui_file_name, + time.time(), + node, + upload_user, + ), + message_thread_id=node.topic_id, + ) + elif message.voice: + if node.reply_to_message: + await node.reply_to_message.reply_voice( + file_name, + caption=caption, + message_thread_id=node.topic_id, + ) + else: + await upload_user.send_voice( + upload_telegram_chat_id, + file_name, + caption=caption, + progress=update_upload_stat, + progress_args=( + message.id, + ui_file_name, + time.time(), + node, + upload_user, + ), + message_thread_id=node.topic_id, + ) + elif message.video_note: + if node.reply_to_message: + await node.reply_to_message.reply_video_note( + file_name, + caption=caption, + message_thread_id=node.topic_id, + ) + else: + await upload_user.send_video_note( + upload_telegram_chat_id, + file_name, + caption=caption, + progress=update_upload_stat, + progress_args=( + message.id, + ui_file_name, + time.time(), + node, + upload_user, + ), + message_thread_id=node.topic_id, + ) + elif message.text: + if node.reply_to_message: + await node.reply_to_message.reply( + message.text, message_thread_id=node.topic_id + ) + else: + await upload_user.send_message( + upload_telegram_chat_id, + message.text, + message_thread_id=node.topic_id, + ) + + +async def _upload_telegram_chat_message( + client: pyrogram.Client, + upload_user: pyrogram.Client, + app: Application, + node: TaskNode, + message: pyrogram.types.Message, + file_name: str = None, +): + """ + Uploads a Telegram chat message to the destination chat. + + Args: + client (pyrogram.Client): The client used to interact with the Telegram API. + upload_user (pyrogram.Client): The client used to upload the message. + app (Application): The application instance. + node (TaskNode): The task node associated with the message. + message (pyrogram.types.Message): The Telegram chat message to be uploaded. + file_name (str): The name of the file to be uploaded. + + Returns: + None + """ + await app.forward_limit_call.wait(node) + + caption = message.caption + caption_entities = message.caption_entities + + # Convert caption and caption_entities to markdown format + if caption and caption_entities: + caption = pyrogram.parser.Parser.unparse(caption, caption_entities, True) + + max_caption_length = 4096 if client.me and client.me.is_premium else 1024 + # proc caption MEDIA_CAPTION_TOO_LONG + if caption and len(caption) > max_caption_length: + caption = caption[:max_caption_length] + + if not message.media_group_id: + if not node.has_protected_content: + if node.reply_to_message: + if message.text: + await node.reply_to_message.reply( + message.text, + message_thread_id=node.topic_id, + ) + elif message.photo: + await node.reply_to_message.reply_photo( + message.photo.file_id, + caption=caption, + message_thread_id=node.topic_id, + ) + elif message.video: + await node.reply_to_message.reply_video( + message.video.file_id, + caption=caption, + message_thread_id=node.topic_id, + ) + elif message.document: + await node.reply_to_message.reply_document( + message.document.file_id, + caption=caption, + message_thread_id=node.topic_id, + ) + elif message.audio: + await node.reply_to_message.reply_audio( + message.audio.file_id, + caption=caption, + message_thread_id=node.topic_id, + ) + else: + # For other types of media, fallback to forward_messages + await forward_messages( + client, + node.upload_telegram_chat_id, + node.chat_id, + message.id, + drop_author=True, + topic_id=node.topic_id, + caption=caption, + ) + else: + await _upload_signal_message( + client, + upload_user, + app, + node, + node.upload_telegram_chat_id, + message, + file_name, + caption, + ) + return ForwardStatus.SuccessForward + + return await forward_multi_media( + client, upload_user, app, node, message, caption, file_name + ) + + +# pylint: disable=R0912 +async def forward_multi_media( + client: pyrogram.Client, + _: pyrogram.Client, + app: Application, + node: TaskNode, + message: pyrogram.types.Message, + caption: str = None, + file_name: str = None, +): + """Forward multi media by cache""" + caption = message.caption + caption_entities = message.caption_entities + if not caption: + caption = app.get_caption_name(node.chat_id, message.media_group_id) + caption_entities = app.get_caption_entities( + node.chat_id, message.media_group_id + ) + + # Convert caption and caption_entities to markdown format + if caption and caption_entities: + caption = pyrogram.parser.Parser.unparse(caption, caption_entities, True) + + max_caption_length = 4096 if client.me and client.me.is_premium else 1024 + # proc caption MEDIA_CAPTION_TOO_LONG + if caption and len(caption) > max_caption_length: + caption = caption[:max_caption_length] + + media_obj = get_media_obj(message, file_name, caption) + if not node.has_protected_content: + media = getattr(message, message.media.value) + if not media: + return ForwardStatus.SkipForward + media_obj.media = media.file_id if media else "" + + if not node.media_group_ids.get(message.media_group_id): + node.media_group_ids[message.media_group_id] = {} + + if not node.media_group_ids[message.media_group_id]: + media_group = await get_media_group_with_retry( + client, node.chat_id, message.id, 5 + ) + if not media_group: + logger.error("Get Media Group Error! message id: {}", message.id) + return ForwardStatus.FailedForward + + for it in media_group: + node.media_group_ids[message.media_group_id][it.id] = None + node.upload_status[message.id] = None + + if not node.media_group_ids[message.media_group_id][message.id]: + node.upload_status[message.id] = UploadStatus.Uploading + try: + ui_file_name = file_name + if file_name: + ui_file_name = ( + f"****{os.path.splitext(file_name)[-1]}" + if app.hide_file_name + else file_name + ) + media_obj.thumb = ( + await download_thumbnail(client, app.temp_save_path, message) + if message.video + else None + ) + + _media = await cache_media( + client, + node.upload_telegram_chat_id, # type: ignore + media_obj, + progress=update_upload_stat, + progress_args=( + message.id, + ui_file_name, + time.time(), + node, + client, + ), + ) + except Exception as e: + logger.exception(f"{e}") + node.upload_status[message.id] = UploadStatus.FailedUpload + finally: + if file_name and message.video and media_obj.thumb: + os.remove(str(media_obj.thumb)) + + if node.upload_status[message.id] == UploadStatus.FailedUpload: + return ForwardStatus.FailedForward + + node.media_group_ids[message.media_group_id][message.id] = _media + node.upload_status[message.id] = UploadStatus.SuccessUpload + + return await proc_cache_forward(client, node, message, bool(file_name)) + + +async def proc_cache_forward( + client: pyrogram.Client, + node: TaskNode, + message: pyrogram.types.Message, + check_download_status: bool, +): + """proc other cache forward""" + if not node.media_group_ids: + return + for key in node.media_group_ids[message.media_group_id].keys(): + download_status = node.download_status.get(key, DownloadStatus.Downloading) + if ( + node.skip_msg_id(key) + or download_status is DownloadStatus.SkipDownload + or download_status is DownloadStatus.FailedDownload + ): + continue + if ( + check_download_status and DownloadStatus.Downloading == download_status + ) or UploadStatus.Uploading == node.upload_status.get( + key, UploadStatus.Uploading + ): + return ForwardStatus.CacheForward + + multi_media: List[pyrogram.raw.types.InputSingleMedia] = [] + + for it in node.media_group_ids[message.media_group_id]: + if node.media_group_ids[message.media_group_id][it]: + if multi_media: + node.media_group_ids[message.media_group_id][it].message = "" + multi_media.append(node.media_group_ids[message.media_group_id][it]) + + forward_status = ForwardStatus.SuccessForward + + reply_to_message_id = None + message_thread_id = node.topic_id + business_connection_id = None + upload_telegram_chat_id = node.upload_telegram_chat_id + if node.reply_to_message: + if node.reply_to_message.chat.type != pyrogram.enums.ChatType.PRIVATE: + reply_to_message_id = node.reply_to_message.id + message_thread_id = node.reply_to_message.message_thread_id + business_connection_id = node.reply_to_message.business_connection_id + upload_telegram_chat_id = node.reply_to_message.chat.id + if not await send_media_group_v2( + client, + upload_telegram_chat_id, # type: ignore + multi_media, + message_thread_id=message_thread_id, + reply_to_message_id=reply_to_message_id, + business_connection_id=business_connection_id, + ): + forward_status = ForwardStatus.FailedForward + + node.stat_forward(forward_status, len(multi_media)) + + node.media_group_ids.pop(message.media_group_id) + return ForwardStatus.CacheForward + + +def record_download_status(func): + """Record download status""" + + @wraps(func) + async def inner( + client: pyrogram.client.Client, + message: pyrogram.types.Message, + media_types: List[str], + file_formats: dict, + node: TaskNode, + ): + if _download_cache[(node.chat_id, message.id)] is DownloadStatus.Downloading: + return DownloadStatus.Downloading, None + + _download_cache[(node.chat_id, message.id)] = DownloadStatus.Downloading + + status, file_name = await func(client, message, media_types, file_formats, node) + + _download_cache[(node.chat_id, message.id)] = status + + return status, file_name + + return inner + + +async def report_bot_download_status( + client: pyrogram.Client, + node: TaskNode, + download_status: DownloadStatus, + download_size: int = 0, +): + """ + Sends a message with the current status of the download bot. + + Parameters: + client (pyrogram.Client): The client instance. + node (TaskNode): The download task node. + download_status (DownloadStatus): The current download status. + + Returns: + None + """ + node.stat(download_status) + node.total_download_byte += download_size + await report_bot_status(client, node) + + +async def report_bot_forward_status( + client: pyrogram.Client, + node: TaskNode, + status: ForwardStatus, +): + """ + Sends a message with the current status of the download bot. + + Parameters: + client (pyrogram.Client): The client instance. + node (TaskNode): The download task node. + status (ForwardStatus): The current forward status. + + Returns: + None + """ + node.stat_forward(status) + await report_bot_status(client, node) + + +async def report_bot_status( + client: pyrogram.Client, + node: TaskNode, + immediate_reply=False, +): + """see _report_bot_status""" + try: + return await _report_bot_status(client, node, immediate_reply) + except Exception as e: + logger.debug(f"{e}") + + +async def _report_bot_status( + client: pyrogram.Client, + node: TaskNode, + immediate_reply=False, +): + """ + Sends a message with the current status of the download bot. + + Parameters: + client (pyrogram.Client): The client instance. + node (TaskNode): The download task node. + immediate_reply(bool): Immediate reply + + Returns: + None + """ + if not node.reply_message_id or not node.bot: + return + + if immediate_reply or node.can_reply(): + if node.upload_telegram_chat_id: + node.forward_msg_detail_str = ( + f"\n🔄 {_t('Forward')}\n" + f"├─ 📁 {_t('Total')}: {node.total_forward_task}\n" + f"├─ ✅ {_t('Success')}: {node.success_forward_task}\n" + f"├─ ❌ {_t('Failed')}: {node.failed_forward_task}\n" + f"└─ ⏩ {_t('Skipped')}: {node.skip_forward_task}\n" + ) + + upload_msg_detail_str: str = "" + + if node.upload_success_count: + upload_msg_detail_str = ( + f"\n☁️ {_t('Upload')}\n" + f"└─ ✅ {_t('Success')}: {node.upload_success_count}\n" + ) + + for idx, value in node.cloud_drive_upload_stat_dict.items(): + if value.transferred == value.total: + continue + + temp_file_name = truncate_filename(os.path.basename(value.file_name), 10) + upload_msg_detail_str += ( + f" ├─ 🆔 {_t('Message ID')}: {idx}\n" + f" │ ├─ 📁 : {temp_file_name}\n" + f" │ ├─ 📏 : {value.total}\n" + f" │ ├─ ⏫ : {value.speed}\n" + f" │ └─ 📊 : [" + f'{create_progress_bar(int(value.percentage.split("%")[0]))}]' + f" ({value.percentage})%\n" + ) + + download_result_str = "" + download_result = get_download_result() + if node.chat_id in download_result: + messages = download_result[node.chat_id] + for idx, value in messages.items(): + task_id = value["task_id"] + if task_id != node.task_id or value["down_byte"] == value["total_size"]: + continue + + temp_file_name = truncate_filename( + os.path.basename(value["file_name"]), 10 + ) + progress = int(value["down_byte"] / value["total_size"] * 100) + download_result_str += ( + f" ├─ 🆔 {_t('Message ID')}: {idx}\n" + f" │ ├─ 📁 : {temp_file_name}\n" + f" │ ├─ 📏 : {format_byte(value['total_size'])}\n" + f" │ ├─ ⏬ : {format_byte(value['download_speed'])}/s\n" + f" │ └─ 📊 : [{create_progress_bar(progress)}]" + f" ({progress}%)\n" + ) + + if download_result_str: + download_result_str = ( + f"\n📥 {_t('Download Progresses')}:\n" + download_result_str + ) + + upload_result_str = "" + for idx, value in node.upload_stat_dict.items(): + if value.total_size == value.upload_size: + continue + + temp_file_name = truncate_filename(os.path.basename(value.file_name), 10) + progress = int(value.upload_size / value.total_size * 100) + upload_result_str += ( + f" ├─ 🆔 {_t('Message ID')}: {idx}\n" + f" │ ├─ 📁 : {temp_file_name}\n" + f" │ ├─ 📏 : {format_byte(value.total_size)}\n" + f" │ ├─ ⏫ : {format_byte(value.upload_speed)}/s\n" + f" │ └─ 📊 : [{create_progress_bar(progress)}]" + f" ({progress}%)\n" + ) + + if upload_result_str: + upload_result_str = f"\n📤 {_t('Upload Progresses')}:\n" + upload_result_str + + new_msg_str = ( + f"`\n" + f"🆔 task id: {node.task_id}\n" + f"📥 {_t('Downloading')}: {format_byte(node.total_download_byte)}\n" + f"├─ 📁 {_t('Total')}: {node.total_download_task}\n" + f"├─ ✅ {_t('Success')}: {node.success_download_task}\n" + f"├─ ❌ {_t('Failed')}: {node.failed_download_task}\n" + f"└─ ⏩ {_t('Skipped')}: {node.skip_download_task}\n" + f"{node.forward_msg_detail_str}" + f"{upload_msg_detail_str}" + f"{upload_result_str}" + f"{download_result_str}\n`" + ) + + if new_msg_str != node.last_edit_msg: + node.last_edit_msg = new_msg_str + await client.edit_message_text( + node.from_user_id, + node.reply_message_id, + new_msg_str, + parse_mode=pyrogram.enums.ParseMode.MARKDOWN, + ) + + +def set_max_concurrent_transmissions( + client: pyrogram.Client, max_concurrent_transmissions: int +): + """Set maximum concurrent transmissions""" + if getattr(client, "max_concurrent_transmissions", None): + client.max_concurrent_transmissions = max_concurrent_transmissions + client.save_file_semaphore = asyncio.Semaphore( + client.max_concurrent_transmissions + ) + client.get_file_semaphore = asyncio.Semaphore( + client.max_concurrent_transmissions + ) + + +async def fetch_message(client: pyrogram.Client, message: pyrogram.types.Message): + """ + This function retrieves a message from a specified chat using the Pyrogram library. + Args: + client (pyrogram.Client): A client instance created using Pyrogram. + message (pyrogram.types.Message): A message instance returned from Pyrogram. + Returns: + pyrogram.types.Message: A message object retrieved from the specified chat. + """ + return await client.get_messages( + chat_id=message.chat.id, + message_ids=message.id, + ) + + +async def retry(func: Callable, args: tuple = (), max_attempts=3, wait_second=15): + """ + Asynchronously retries the provided function + a specified number of times with a specified wait time between retries. + + :param func: The function to be retried. + :param args: The arguments to be passed to the function. + :param max_attempts: The maximum number of attempts to retry the function. + Defaults to 3. + :param wait_second: The wait time in seconds between each retry attempt. + Defaults to 15. + + :return: The result of the function + if it succeeds within the maximum number of attempts, otherwise None. + """ + + for _ in range(1, max_attempts + 1): + try: + return await func(*args) + except pyrogram.errors.exceptions.flood_420.FloodWait as wait_err: + logger.warning("bad call retry: FlowWait {}", wait_err.value) + await asyncio.sleep(wait_err.value) + except Exception as e: + logger.exception("Error: {}", e) + await asyncio.sleep(wait_second) + + logger.error("Failed after {} attempts", max_attempts) + return None + + +async def get_media_group_with_retry( + client: pyrogram.Client, + chat_id: Union[int, str], + message_id: int, + max_attempts: int = 3, + wait_second: int = 15, +): + """ + get_media_group_with_retry + """ + for attempt in range(1, max_attempts + 1): + try: + return await client.get_media_group(chat_id, message_id) + except Exception as e: + if attempt == max_attempts: + logger.error("Failed Get Media Group[{}]", message_id) + return types.List() + + logger.exception("Get Message[{}]: Error {}", message_id, e) + await asyncio.sleep(wait_second) + return types.List() + + +async def check_user_permission( + client: pyrogram.Client, user_id: Union[int, str], chat_id: Union[int, str] +) -> bool: + """ + Check if the user has permission to send videos in the group. + + Args: + client (pyrogram.Client): A client instance created using Pyrogram. + user_id (Union[int, str]): User Id + chat_id (Union[int, str]): Chat Id + + Returns: + if can_send_media_messages return True + """ + try: + member = await client.get_chat_member(chat_id, user_id) + return member and ( + not member.permissions or member.permissions.can_send_media_messages + ) + except Exception: + # logger.exception(e) + pass + + return False + + +def set_meta_data( + meta_data: MetaData, message: pyrogram.types.Message, caption: str = None +): + """Get all meta data""" + # message + meta_data.message_date = getattr(message, "date", None) + if caption: + meta_data.message_caption = caption + else: + meta_data.message_caption = getattr(message, "caption", None) or "" + meta_data.message_id = getattr(message, "id", None) + + from_user = getattr(message, "from_user") + meta_data.sender_id = from_user.id if from_user else 0 + meta_data.sender_name = (from_user.username if from_user else "") or "" + meta_data.reply_to_message_id = getattr( + message, "reply_to_message_id", 1 + ) # 1 for General + + meta_data.message_thread_id = getattr(message, "message_thread_id", 1) + # media + for kind in meta_data.AVAILABLE_MEDIA: + media_obj = getattr(message, kind, None) + if media_obj is not None: + meta_data.media_type = kind + break + else: + return + meta_data.media_file_name = getattr(media_obj, "file_name", None) or "" + meta_data.media_file_size = getattr(media_obj, "file_size", None) + meta_data.media_width = getattr(media_obj, "width", None) + meta_data.media_height = getattr(media_obj, "height", None) + meta_data.media_duration = getattr(media_obj, "duration", None) + meta_data.file_extension = get_extension( + media_obj.file_id, getattr(media_obj, "mime_type", ""), False + ) + + +async def parse_link(client: pyrogram.Client, link_str: str): + """Parse link""" + link = extract_info_from_link(link_str) + if link.comment_id: + chat = await client.get_chat(link.group_id) + if chat: + return chat.linked_chat.id, link.comment_id, link.topic_id + + return link.group_id, link.post_id, link.topic_id + + +async def update_cloud_upload_stat( + transferred: str, + total: str, + percentage: str, + speed: str, + eta: str, + node: TaskNode, + message_id: int, + file_name: str, +): + """ + Update the cloud upload statistics with the given information. + + Args: + transferred (str): The amount of data transferred. + total (str): The total size of the file. + percentage (str): The percentage of the file uploaded. + speed (str): The upload speed. + eta (str): The estimated time of arrival for the upload to complete. + node (TaskNode): The task node associated with the upload. + message_id (int): The ID of the message. + file_name (str): The name of the file being uploaded. + + Returns: + None + """ + node.cloud_drive_upload_stat_dict[message_id] = CloudDriveUploadStat( + file_name=file_name, + transferred=transferred, + total=total, + percentage=percentage, + speed=speed, + eta=eta, + ) + + +async def update_upload_stat( + upload_size: int, + total_size: int, + message_id: int, + file_name: str, + start_time: float, + node: TaskNode, + client: pyrogram.Client, +): + """update_upload_status""" + cur_time = time.time() + + if node.is_stop_transmission: + client.stop_transmission() + + # TODO(tyh): web control upload stop + + if node.upload_stat_dict.get(message_id): + upload_stat = node.upload_stat_dict[message_id] + + if cur_time - upload_stat.last_stat_time >= 1.0: + upload_stat.upload_speed = max( + int( + (upload_size - upload_stat.upload_size) + / (cur_time - upload_stat.last_stat_time) + ), + 0, + ) + upload_stat.last_stat_time = cur_time + upload_stat.upload_size = upload_size + + node.upload_stat_dict[message_id] = upload_stat + else: + upload_stat = UploadProgressStat( + file_name=file_name, + total_size=total_size, + upload_size=upload_size, + start_time=start_time, + last_stat_time=cur_time, + upload_speed=upload_size / (cur_time - start_time), + ) + node.upload_stat_dict[message_id] = upload_stat + + +# pylint: enable=W0201 +class HookSession(pyrogram.session.Session): + """Hook Session""" + + def start_timeout(self: pyrogram.session.Session, start_timeout: int): + """ + Set the start timeout for the session. + + Args: + start_timeout (int): The start timeout value in seconds. + + Returns: + None + """ + self.START_TIMEOUT = start_timeout + + +# pylint: disable=all +class HookClient(pyrogram.Client): + """Hook Client""" + + # pylint: disable=R0901 + START_TIME_OUT = 60 + + def __init__(self, name: str, **kwargs): + if "start_timeout" in kwargs: + value = kwargs.get("start_timeout") + if value: + self.START_TIME_OUT = value + kwargs.pop("start_timeout") + + super().__init__(name, **kwargs) + + async def connect( + self, + ) -> bool: + """ + Connects the client to the server. + + Returns: + bool: True if the client successfully + connects to the server, False otherwise. + + Raises: + ConnectionError: If the client is already connected. + + """ + if self.is_connected: # type: ignore + raise ConnectionError("Client is already connected") + + await self.load_session() + + self.session = HookSession( + self, + await self.storage.dc_id(), + await self.storage.auth_key(), + await self.storage.test_mode(), + ) + self.session.start_timeout(self.START_TIME_OUT) + + await self.session.start() + + self.is_connected = True + + return bool(await self.storage.user_id()) + + async def start(self): + """ + Starts the client by performing necessary initialization steps. + + Returns: + The initialized client instance. + """ + is_authorized = await self.connect() + + try: + if not is_authorized: + await self.authorize() + + if not await self.storage.is_bot() and self.takeout: + self.takeout_id = ( + await self.invoke( + pyrogram.raw.functions.account.InitTakeoutSession() + ) + ).id + logger.warning(f"Takeout session {self.takeout_id} initiated") + + await self.invoke(pyrogram.raw.functions.updates.GetState()) + except (Exception, KeyboardInterrupt): + await self.disconnect() + raise + else: + self.me = await self.get_me() + await self.initialize() + + return self + + +# pylint: disable=R0914,R0913 +async def forward_messages( + client: pyrogram.Client, + chat_id: Union[int, str, None], + from_chat_id: Union[int, str], + message_ids: Union[int, Iterable[int]], + disable_notification: bool = None, + schedule_date: datetime = None, + protect_content: bool = None, + drop_author: bool = None, + topic_id: int = None, + caption: str = None, + caption_entities: List[pyrogram.types.MessageEntity] = None, +) -> Union["types.Message", List["types.Message"]]: + """Forward messages of any kind.""" + + is_iterable = not isinstance(message_ids, int) + message_ids = list(message_ids) if is_iterable else [message_ids] # type: ignore + + r = await client.invoke( + pyrogram.raw.functions.messages.ForwardMessages( + to_peer=await client.resolve_peer(chat_id), + from_peer=await client.resolve_peer(from_chat_id), + id=message_ids, + silent=disable_notification or None, + random_id=[client.rnd_id() for _ in message_ids], + schedule_date=pyrogram.utils.datetime_to_timestamp(schedule_date), + noforwards=protect_content, + drop_author=drop_author, + top_msg_id=topic_id, + entities=caption_entities, + ) + ) + + forwarded_messages = [] + + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + for i in r.updates: + if isinstance( + i, + ( + pyrogram.raw.types.UpdateNewMessage, + pyrogram.raw.types.UpdateNewChannelMessage, + pyrogram.raw.types.UpdateNewScheduledMessage, + ), + ): + forwarded_messages.append( + # pylint: disable=W0212 + await types.Message._parse(client, i.message, users, chats) + ) + + if caption and not is_iterable and forwarded_messages: + await client.edit_message_caption( + chat_id, forwarded_messages[0].id, caption=caption + ) + + return types.List(forwarded_messages) if is_iterable else forwarded_messages[0] diff --git a/module/send_media_group_v2.py b/module/send_media_group_v2.py new file mode 100644 index 0000000..c37f9a7 --- /dev/null +++ b/module/send_media_group_v2.py @@ -0,0 +1,431 @@ +"""send media group""" +import logging +import os +import re +from datetime import datetime +from typing import Callable, List, Union + +import pyrogram +from pyrogram import raw, types, utils +from pyrogram.file_id import FileType + +log = logging.getLogger(__name__) + +# pylint: disable = R0915, R0902, R0912 +async def cache_media( + client: pyrogram.Client, + chat_id: Union[int, str], + media_obj: Union[ + "types.InputMediaPhoto", + "types.InputMediaVideo", + "types.InputMediaAudio", + "types.InputMediaDocument", + ], + progress: Callable = None, + progress_args: tuple = (), +) -> raw.base.InputSingleMedia: + """ + Caches a media. + + :param client: The pyrogram.Client instance. + :param chat_id: The ID of the chat. + :param media: The media to be cached. + :return: The cached media. + """ + if isinstance(media_obj, types.InputMediaPhoto): + if isinstance(media_obj.media, str): + if os.path.isfile(media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedPhoto( + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ) + ), + ) + ) + + media = raw.types.InputMediaPhoto( + id=raw.types.InputPhoto( + id=media.photo.id, + access_hash=media.photo.access_hash, + file_reference=media.photo.file_reference, + ) + ) + elif re.match("^https?://", media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaPhotoExternal(url=media_obj.media), + ) + ) + + media = raw.types.InputMediaPhoto( + id=raw.types.InputPhoto( + id=media.photo.id, + access_hash=media.photo.access_hash, + file_reference=media.photo.file_reference, + ) + ) + else: + media = utils.get_input_media_from_file_id( + media_obj.media, FileType.PHOTO + ) + else: + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedPhoto( + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ) + ), + ) + ) + + media = raw.types.InputMediaPhoto( + id=raw.types.InputPhoto( + id=media.photo.id, + access_hash=media.photo.access_hash, + file_reference=media.photo.file_reference, + ) + ) + elif isinstance(media_obj, types.InputMediaVideo): + if isinstance(media_obj.media, str): + if os.path.isfile(media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ), + thumb=await client.save_file(media_obj.thumb), + mime_type=client.guess_mime_type(media_obj.media) + or "video/mp4", + nosound_video=True, + attributes=[ + raw.types.DocumentAttributeVideo( + supports_streaming=media_obj.supports_streaming + or None, + duration=media_obj.duration, + w=media_obj.width, + h=media_obj.height, + ), + raw.types.DocumentAttributeFilename( + file_name=os.path.basename(media_obj.media) + ), + ], + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + elif re.match("^https?://", media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaDocumentExternal(url=media_obj.media), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + else: + media = utils.get_input_media_from_file_id( + media_obj.media, FileType.VIDEO + ) + else: + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ), + thumb=await client.save_file(media_obj.thumb), + mime_type=client.guess_mime_type( + getattr(media_obj.media, "name", "video.mp4") + ) + or "video/mp4", + nosound_video=True, + attributes=[ + raw.types.DocumentAttributeVideo( + supports_streaming=media_obj.supports_streaming or None, + duration=media_obj.duration, + w=media_obj.width, + h=media_obj.height, + ), + raw.types.DocumentAttributeFilename( + file_name=getattr(media_obj.media, "name", "video.mp4") + ), + ], + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + elif isinstance(media_obj, types.InputMediaAudio): + if isinstance(media_obj.media, str): + if os.path.isfile(media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + mime_type=client.guess_mime_type(media_obj.media) + or "audio/mpeg", + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ), + thumb=await client.save_file(media_obj.thumb), + attributes=[ + raw.types.DocumentAttributeAudio( + duration=media_obj.duration, + performer=media_obj.performer, + title=media_obj.title, + ), + raw.types.DocumentAttributeFilename( + file_name=os.path.basename(media_obj.media) + ), + ], + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + elif re.match("^https?://", media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaDocumentExternal(url=media_obj.media), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + else: + media = utils.get_input_media_from_file_id( + media_obj.media, FileType.AUDIO + ) + else: + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + mime_type=client.guess_mime_type( + getattr(media_obj.media, "name", "audio.mp3") + ) + or "audio/mpeg", + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ), + thumb=await client.save_file(media_obj.thumb), + attributes=[ + raw.types.DocumentAttributeAudio( + duration=media_obj.duration, + performer=media_obj.performer, + title=media_obj.title, + ), + raw.types.DocumentAttributeFilename( + file_name=getattr(media_obj.media, "name", "audio.mp3") + ), + ], + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + elif isinstance(media_obj, types.InputMediaDocument): + if isinstance(media_obj.media, str): + if os.path.isfile(media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + mime_type=client.guess_mime_type(media_obj.media) + or "application/zip", + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ), + thumb=await client.save_file(media_obj.thumb), + attributes=[ + raw.types.DocumentAttributeFilename( + file_name=os.path.basename(media_obj.media) + ) + ], + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + elif re.match("^https?://", media_obj.media): + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaDocumentExternal(url=media_obj.media), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + else: + media = utils.get_input_media_from_file_id( + media_obj.media, FileType.DOCUMENT + ) + else: + media = await client.invoke( + raw.functions.messages.UploadMedia( + peer=await client.resolve_peer(chat_id), + media=raw.types.InputMediaUploadedDocument( + mime_type=client.guess_mime_type( + getattr(media_obj.media, "name", "file.zip") + ) + or "application/zip", + file=await client.save_file( + media_obj.media, + progress=progress, + progress_args=progress_args, + ), + thumb=await client.save_file(media_obj.thumb), + attributes=[ + raw.types.DocumentAttributeFilename( + file_name=getattr(media_obj.media, "name", "file.zip") + ) + ], + ), + ) + ) + + media = raw.types.InputMediaDocument( + id=raw.types.InputDocument( + id=media.document.id, + access_hash=media.document.access_hash, + file_reference=media.document.file_reference, + ) + ) + else: + raise ValueError( + f"{media_obj.__class__.__name__}" + " is not a supported type for send_media_group" + ) + + return raw.types.InputSingleMedia( + media=media, + random_id=client.rnd_id(), + **await utils.parse_text_entities( + client, media_obj.caption, media_obj.parse_mode, media_obj.caption_entities + ), + ) + + +async def send_media_group_v2( + client: pyrogram.Client, + chat_id: Union[int, str], + multi_media: List[raw.types.InputSingleMedia], + disable_notification: bool = None, + schedule_date: datetime = None, + protect_content: bool = None, + message_thread_id: int = None, + reply_to_message_id: int = None, + business_connection_id: int = None, +): + """ + see pyrogram + """ + r = await client.invoke( + raw.functions.messages.SendMultiMedia( + peer=await client.resolve_peer(chat_id), + multi_media=multi_media, + silent=disable_notification or None, + schedule_date=utils.datetime_to_timestamp(schedule_date), + noforwards=protect_content, + reply_to=utils.get_reply_to( + message_thread_id=message_thread_id, + reply_to_message_id=reply_to_message_id, + ), + ), + sleep_threshold=60, + business_connection_id=business_connection_id, + ) + + return await utils.parse_messages( + client, + raw.types.messages.Messages( + messages=[ + m.message + for m in filter( + lambda u: isinstance( + u, + ( + raw.types.UpdateNewMessage, + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewScheduledMessage, + ), + ), + r.updates, + ) + ], + users=r.users, + chats=r.chats, + ), + ) diff --git a/module/static/aes/crypto-js-master/CONTRIBUTING.md b/module/static/aes/crypto-js-master/CONTRIBUTING.md new file mode 100644 index 0000000..8fab0f0 --- /dev/null +++ b/module/static/aes/crypto-js-master/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# Contribution + +# Git Flow + +The crypto-js project uses [git flow](https://github.com/nvie/gitflow) to manage branches. +Do your changes on the `develop` or even better on a `feature/*` branch. Don't do any changes on the `master` branch. + +# Pull request + +Target your pull request on `develop` branch. Other pull request won't be accepted. + +# How to build + +1. Clone + +2. Run + + ```sh + npm install + ``` + +3. Run + + ```sh + npm run build + ``` + +4. Check `build` folder diff --git a/module/static/aes/crypto-js-master/LICENSE b/module/static/aes/crypto-js-master/LICENSE new file mode 100644 index 0000000..3f98399 --- /dev/null +++ b/module/static/aes/crypto-js-master/LICENSE @@ -0,0 +1,24 @@ +# License + +[The MIT License (MIT)](http://opensource.org/licenses/MIT) + +Copyright (c) 2009-2013 Jeff Mott +Copyright (c) 2013-2016 Evan Vosberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/module/static/aes/crypto-js-master/README.md b/module/static/aes/crypto-js-master/README.md new file mode 100644 index 0000000..692ead6 --- /dev/null +++ b/module/static/aes/crypto-js-master/README.md @@ -0,0 +1,259 @@ +# crypto-js [![Build Status](https://travis-ci.org/brix/crypto-js.svg?branch=develop)](https://travis-ci.org/brix/crypto-js) + +JavaScript library of crypto standards. + +## Node.js (Install) + +Requirements: + +- Node.js +- npm (Node.js package manager) + +```bash +npm install crypto-js +``` + +### Usage + +ES6 import for typical API call signing use case: + +```javascript +import sha256 from 'crypto-js/sha256'; +import hmacSHA512 from 'crypto-js/hmac-sha512'; +import Base64 from 'crypto-js/enc-base64'; + +const message, nonce, path, privateKey; // ... +const hashDigest = sha256(nonce + message); +const hmacDigest = Base64.stringify(hmacSHA512(path + hashDigest, privateKey)); +``` + +Modular include: + +```javascript +var AES = require("crypto-js/aes"); +var SHA256 = require("crypto-js/sha256"); +... +console.log(SHA256("Message")); +``` + +Including all libraries, for access to extra methods: + +```javascript +var CryptoJS = require("crypto-js"); +console.log(CryptoJS.HmacSHA1("Message", "Key")); +``` + +## Client (browser) + +Requirements: + +- Node.js +- Bower (package manager for frontend) + +```bash +bower install crypto-js +``` + +### Usage + +Modular include: + +```javascript +require.config({ + packages: [ + { + name: 'crypto-js', + location: 'path-to/bower_components/crypto-js', + main: 'index' + } + ] +}); + +require(["crypto-js/aes", "crypto-js/sha256"], function (AES, SHA256) { + console.log(SHA256("Message")); +}); +``` + +Including all libraries, for access to extra methods: + +```javascript +// Above-mentioned will work or use this simple form +require.config({ + paths: { + 'crypto-js': 'path-to/bower_components/crypto-js/crypto-js' + } +}); + +require(["crypto-js"], function (CryptoJS) { + console.log(CryptoJS.HmacSHA1("Message", "Key")); +}); +``` + +### Usage without RequireJS + +```html + + +``` + +## API + +See: https://cryptojs.gitbook.io/docs/ + +### AES Encryption + +#### Plain text encryption + +```javascript +var CryptoJS = require("crypto-js"); + +// Encrypt +var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString(); + +// Decrypt +var bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123'); +var originalText = bytes.toString(CryptoJS.enc.Utf8); + +console.log(originalText); // 'my message' +``` + +#### Object encryption + +```javascript +var CryptoJS = require("crypto-js"); + +var data = [{id: 1}, {id: 2}] + +// Encrypt +var ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), 'secret key 123').toString(); + +// Decrypt +var bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123'); +var decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + +console.log(decryptedData); // [{id: 1}, {id: 2}] +``` + +### List of modules + + +- ```crypto-js/core``` +- ```crypto-js/x64-core``` +- ```crypto-js/lib-typedarrays``` + +--- + +- ```crypto-js/md5``` +- ```crypto-js/sha1``` +- ```crypto-js/sha256``` +- ```crypto-js/sha224``` +- ```crypto-js/sha512``` +- ```crypto-js/sha384``` +- ```crypto-js/sha3``` +- ```crypto-js/ripemd160``` + +--- + +- ```crypto-js/hmac-md5``` +- ```crypto-js/hmac-sha1``` +- ```crypto-js/hmac-sha256``` +- ```crypto-js/hmac-sha224``` +- ```crypto-js/hmac-sha512``` +- ```crypto-js/hmac-sha384``` +- ```crypto-js/hmac-sha3``` +- ```crypto-js/hmac-ripemd160``` + +--- + +- ```crypto-js/pbkdf2``` + +--- + +- ```crypto-js/aes``` +- ```crypto-js/tripledes``` +- ```crypto-js/rc4``` +- ```crypto-js/rabbit``` +- ```crypto-js/rabbit-legacy``` +- ```crypto-js/evpkdf``` + +--- + +- ```crypto-js/format-openssl``` +- ```crypto-js/format-hex``` + +--- + +- ```crypto-js/enc-latin1``` +- ```crypto-js/enc-utf8``` +- ```crypto-js/enc-hex``` +- ```crypto-js/enc-utf16``` +- ```crypto-js/enc-base64``` + +--- + +- ```crypto-js/mode-cfb``` +- ```crypto-js/mode-ctr``` +- ```crypto-js/mode-ctr-gladman``` +- ```crypto-js/mode-ofb``` +- ```crypto-js/mode-ecb``` + +--- + +- ```crypto-js/pad-pkcs7``` +- ```crypto-js/pad-ansix923``` +- ```crypto-js/pad-iso10126``` +- ```crypto-js/pad-iso97971``` +- ```crypto-js/pad-zeropadding``` +- ```crypto-js/pad-nopadding``` + + +## Release notes + +### 4.1.1 + +Fix module order in bundled release. + +Include the browser field in the released package.json. + +### 4.1.0 + +Added url safe variant of base64 encoding. [357](https://github.com/brix/crypto-js/pull/357) + +Avoid webpack to add crypto-browser package. [364](https://github.com/brix/crypto-js/pull/364) + +### 4.0.0 + +This is an update including breaking changes for some environments. + +In this version `Math.random()` has been replaced by the random methods of the native crypto module. + +For this reason CryptoJS might not run in some JavaScript environments without native crypto module. Such as IE 10 or before or React Native. + +### 3.3.0 + +Rollback, `3.3.0` is the same as `3.1.9-1`. + +The move of using native secure crypto module will be shifted to a new `4.x.x` version. As it is a breaking change the impact is too big for a minor release. + +### 3.2.1 + +The usage of the native crypto module has been fixed. The import and access of the native crypto module has been improved. + +### 3.2.0 + +In this version `Math.random()` has been replaced by the random methods of the native crypto module. + +For this reason CryptoJS might does not run in some JavaScript environments without native crypto module. Such as IE 10 or before. + +If it's absolute required to run CryptoJS in such an environment, stay with `3.1.x` version. Encrypting and decrypting stays compatible. But keep in mind `3.1.x` versions still use `Math.random()` which is cryptographically not secure, as it's not random enough. + +This version came along with `CRITICAL` `BUG`. + +DO NOT USE THIS VERSION! Please, go for a newer version! + +### 3.1.x + +The `3.1.x` are based on the original CryptoJS, wrapped in CommonJS modules. diff --git a/module/static/aes/crypto-js-master/aes.js b/module/static/aes/crypto-js-master/aes.js new file mode 100644 index 0000000..156e2c7 --- /dev/null +++ b/module/static/aes/crypto-js-master/aes.js @@ -0,0 +1,234 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./enc-base64"), require("./md5"), require("./evpkdf"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./enc-base64", "./md5", "./evpkdf", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var BlockCipher = C_lib.BlockCipher; + var C_algo = C.algo; + + // Lookup tables + var SBOX = []; + var INV_SBOX = []; + var SUB_MIX_0 = []; + var SUB_MIX_1 = []; + var SUB_MIX_2 = []; + var SUB_MIX_3 = []; + var INV_SUB_MIX_0 = []; + var INV_SUB_MIX_1 = []; + var INV_SUB_MIX_2 = []; + var INV_SUB_MIX_3 = []; + + // Compute lookup tables + (function () { + // Compute double table + var d = []; + for (var i = 0; i < 256; i++) { + if (i < 128) { + d[i] = i << 1; + } else { + d[i] = (i << 1) ^ 0x11b; + } + } + + // Walk GF(2^8) + var x = 0; + var xi = 0; + for (var i = 0; i < 256; i++) { + // Compute sbox + var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4); + sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63; + SBOX[x] = sx; + INV_SBOX[sx] = x; + + // Compute multiplication + var x2 = d[x]; + var x4 = d[x2]; + var x8 = d[x4]; + + // Compute sub bytes, mix columns tables + var t = (d[sx] * 0x101) ^ (sx * 0x1010100); + SUB_MIX_0[x] = (t << 24) | (t >>> 8); + SUB_MIX_1[x] = (t << 16) | (t >>> 16); + SUB_MIX_2[x] = (t << 8) | (t >>> 24); + SUB_MIX_3[x] = t; + + // Compute inv sub bytes, inv mix columns tables + var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100); + INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8); + INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16); + INV_SUB_MIX_2[sx] = (t << 8) | (t >>> 24); + INV_SUB_MIX_3[sx] = t; + + // Compute next counter + if (!x) { + x = xi = 1; + } else { + x = x2 ^ d[d[d[x8 ^ x2]]]; + xi ^= d[d[xi]]; + } + } + }()); + + // Precomputed Rcon lookup + var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + + /** + * AES block cipher algorithm. + */ + var AES = C_algo.AES = BlockCipher.extend({ + _doReset: function () { + var t; + + // Skip reset of nRounds has been set before and key did not change + if (this._nRounds && this._keyPriorReset === this._key) { + return; + } + + // Shortcuts + var key = this._keyPriorReset = this._key; + var keyWords = key.words; + var keySize = key.sigBytes / 4; + + // Compute number of rounds + var nRounds = this._nRounds = keySize + 6; + + // Compute number of key schedule rows + var ksRows = (nRounds + 1) * 4; + + // Compute key schedule + var keySchedule = this._keySchedule = []; + for (var ksRow = 0; ksRow < ksRows; ksRow++) { + if (ksRow < keySize) { + keySchedule[ksRow] = keyWords[ksRow]; + } else { + t = keySchedule[ksRow - 1]; + + if (!(ksRow % keySize)) { + // Rot word + t = (t << 8) | (t >>> 24); + + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + + // Mix Rcon + t ^= RCON[(ksRow / keySize) | 0] << 24; + } else if (keySize > 6 && ksRow % keySize == 4) { + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + } + + keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t; + } + } + + // Compute inv key schedule + var invKeySchedule = this._invKeySchedule = []; + for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) { + var ksRow = ksRows - invKsRow; + + if (invKsRow % 4) { + var t = keySchedule[ksRow]; + } else { + var t = keySchedule[ksRow - 4]; + } + + if (invKsRow < 4 || ksRow <= 4) { + invKeySchedule[invKsRow] = t; + } else { + invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^ + INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]]; + } + } + }, + + encryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX); + }, + + decryptBlock: function (M, offset) { + // Swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + + this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX); + + // Inv swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + }, + + _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) { + // Shortcut + var nRounds = this._nRounds; + + // Get input, add round key + var s0 = M[offset] ^ keySchedule[0]; + var s1 = M[offset + 1] ^ keySchedule[1]; + var s2 = M[offset + 2] ^ keySchedule[2]; + var s3 = M[offset + 3] ^ keySchedule[3]; + + // Key schedule row counter + var ksRow = 4; + + // Rounds + for (var round = 1; round < nRounds; round++) { + // Shift rows, sub bytes, mix columns, add round key + var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++]; + var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++]; + var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++]; + var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++]; + + // Update state + s0 = t0; + s1 = t1; + s2 = t2; + s3 = t3; + } + + // Shift rows, sub bytes, add round key + var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++]; + var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++]; + var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++]; + var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++]; + + // Set output + M[offset] = t0; + M[offset + 1] = t1; + M[offset + 2] = t2; + M[offset + 3] = t3; + }, + + keySize: 256/32 + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.AES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.AES.decrypt(ciphertext, key, cfg); + */ + C.AES = BlockCipher._createHelper(AES); + }()); + + + return CryptoJS.AES; + +})); diff --git a/module/static/aes/crypto-js-master/bower.json b/module/static/aes/crypto-js-master/bower.json new file mode 100644 index 0000000..1e12fdc --- /dev/null +++ b/module/static/aes/crypto-js-master/bower.json @@ -0,0 +1,39 @@ +{ + "name": "crypto-js", + "version": "4.1.1", + "description": "JavaScript library of crypto standards.", + "license": "MIT", + "homepage": "http://github.com/brix/crypto-js", + "repository": { + "type": "git", + "url": "http://github.com/brix/crypto-js.git" + }, + "keywords": [ + "security", + "crypto", + "Hash", + "MD5", + "SHA1", + "SHA-1", + "SHA256", + "SHA-256", + "RC4", + "Rabbit", + "AES", + "DES", + "PBKDF2", + "HMAC", + "OFB", + "CFB", + "CTR", + "CBC", + "Base64", + "Base64url" + ], + "main": "index.js", + "dependencies": {}, + "browser": { + "crypto": false + }, + "ignore": [] +} diff --git a/module/static/aes/crypto-js-master/cipher-core.js b/module/static/aes/crypto-js-master/cipher-core.js new file mode 100644 index 0000000..ebc61f6 --- /dev/null +++ b/module/static/aes/crypto-js-master/cipher-core.js @@ -0,0 +1,890 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./evpkdf")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./evpkdf"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * Cipher core components. + */ + CryptoJS.lib.Cipher || (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var Base64 = C_enc.Base64; + var C_algo = C.algo; + var EvpKDF = C_algo.EvpKDF; + + /** + * Abstract base cipher template. + * + * @property {number} keySize This cipher's key size. Default: 4 (128 bits) + * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits) + * @property {number} _ENC_XFORM_MODE A constant representing encryption mode. + * @property {number} _DEC_XFORM_MODE A constant representing decryption mode. + */ + var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + * + * @property {WordArray} iv The IV to use for this operation. + */ + cfg: Base.extend(), + + /** + * Creates this cipher in encryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray }); + */ + createEncryptor: function (key, cfg) { + return this.create(this._ENC_XFORM_MODE, key, cfg); + }, + + /** + * Creates this cipher in decryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray }); + */ + createDecryptor: function (key, cfg) { + return this.create(this._DEC_XFORM_MODE, key, cfg); + }, + + /** + * Initializes a newly created cipher. + * + * @param {number} xformMode Either the encryption or decryption transormation mode constant. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @example + * + * var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray }); + */ + init: function (xformMode, key, cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Store transform mode and key + this._xformMode = xformMode; + this._key = key; + + // Set initial values + this.reset(); + }, + + /** + * Resets this cipher to its initial state. + * + * @example + * + * cipher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-cipher logic + this._doReset(); + }, + + /** + * Adds data to be encrypted or decrypted. + * + * @param {WordArray|string} dataUpdate The data to encrypt or decrypt. + * + * @return {WordArray} The data after processing. + * + * @example + * + * var encrypted = cipher.process('data'); + * var encrypted = cipher.process(wordArray); + */ + process: function (dataUpdate) { + // Append + this._append(dataUpdate); + + // Process available blocks + return this._process(); + }, + + /** + * Finalizes the encryption or decryption process. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt. + * + * @return {WordArray} The data after final processing. + * + * @example + * + * var encrypted = cipher.finalize(); + * var encrypted = cipher.finalize('data'); + * var encrypted = cipher.finalize(wordArray); + */ + finalize: function (dataUpdate) { + // Final data update + if (dataUpdate) { + this._append(dataUpdate); + } + + // Perform concrete-cipher logic + var finalProcessedData = this._doFinalize(); + + return finalProcessedData; + }, + + keySize: 128/32, + + ivSize: 128/32, + + _ENC_XFORM_MODE: 1, + + _DEC_XFORM_MODE: 2, + + /** + * Creates shortcut functions to a cipher's object interface. + * + * @param {Cipher} cipher The cipher to create a helper for. + * + * @return {Object} An object with encrypt and decrypt shortcut functions. + * + * @static + * + * @example + * + * var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES); + */ + _createHelper: (function () { + function selectCipherStrategy(key) { + if (typeof key == 'string') { + return PasswordBasedCipher; + } else { + return SerializableCipher; + } + } + + return function (cipher) { + return { + encrypt: function (message, key, cfg) { + return selectCipherStrategy(key).encrypt(cipher, message, key, cfg); + }, + + decrypt: function (ciphertext, key, cfg) { + return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg); + } + }; + }; + }()) + }); + + /** + * Abstract base stream cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits) + */ + var StreamCipher = C_lib.StreamCipher = Cipher.extend({ + _doFinalize: function () { + // Process partial blocks + var finalProcessedBlocks = this._process(!!'flush'); + + return finalProcessedBlocks; + }, + + blockSize: 1 + }); + + /** + * Mode namespace. + */ + var C_mode = C.mode = {}; + + /** + * Abstract base block cipher mode template. + */ + var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({ + /** + * Creates this mode for encryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words); + */ + createEncryptor: function (cipher, iv) { + return this.Encryptor.create(cipher, iv); + }, + + /** + * Creates this mode for decryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words); + */ + createDecryptor: function (cipher, iv) { + return this.Decryptor.create(cipher, iv); + }, + + /** + * Initializes a newly created mode. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words); + */ + init: function (cipher, iv) { + this._cipher = cipher; + this._iv = iv; + } + }); + + /** + * Cipher Block Chaining mode. + */ + var CBC = C_mode.CBC = (function () { + /** + * Abstract base CBC mode. + */ + var CBC = BlockCipherMode.extend(); + + /** + * CBC encryptor. + */ + CBC.Encryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // XOR and encrypt + xorBlock.call(this, words, offset, blockSize); + cipher.encryptBlock(words, offset); + + // Remember this block to use with next block + this._prevBlock = words.slice(offset, offset + blockSize); + } + }); + + /** + * CBC decryptor. + */ + CBC.Decryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // Remember this block to use with next block + var thisBlock = words.slice(offset, offset + blockSize); + + // Decrypt and XOR + cipher.decryptBlock(words, offset); + xorBlock.call(this, words, offset, blockSize); + + // This block becomes the previous block + this._prevBlock = thisBlock; + } + }); + + function xorBlock(words, offset, blockSize) { + var block; + + // Shortcut + var iv = this._iv; + + // Choose mixing block + if (iv) { + block = iv; + + // Remove IV for subsequent blocks + this._iv = undefined; + } else { + block = this._prevBlock; + } + + // XOR blocks + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= block[i]; + } + } + + return CBC; + }()); + + /** + * Padding namespace. + */ + var C_pad = C.pad = {}; + + /** + * PKCS #5/7 padding strategy. + */ + var Pkcs7 = C_pad.Pkcs7 = { + /** + * Pads data using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to pad. + * @param {number} blockSize The multiple that the data should be padded to. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.pad(wordArray, 4); + */ + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Create padding word + var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes; + + // Create padding + var paddingWords = []; + for (var i = 0; i < nPaddingBytes; i += 4) { + paddingWords.push(paddingWord); + } + var padding = WordArray.create(paddingWords, nPaddingBytes); + + // Add padding + data.concat(padding); + }, + + /** + * Unpads data that had been padded using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to unpad. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.unpad(wordArray); + */ + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + /** + * Abstract base block cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits) + */ + var BlockCipher = C_lib.BlockCipher = Cipher.extend({ + /** + * Configuration options. + * + * @property {Mode} mode The block mode to use. Default: CBC + * @property {Padding} padding The padding strategy to use. Default: Pkcs7 + */ + cfg: Cipher.cfg.extend({ + mode: CBC, + padding: Pkcs7 + }), + + reset: function () { + var modeCreator; + + // Reset cipher + Cipher.reset.call(this); + + // Shortcuts + var cfg = this.cfg; + var iv = cfg.iv; + var mode = cfg.mode; + + // Reset block mode + if (this._xformMode == this._ENC_XFORM_MODE) { + modeCreator = mode.createEncryptor; + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + modeCreator = mode.createDecryptor; + // Keep at least one block in the buffer for unpadding + this._minBufferSize = 1; + } + + if (this._mode && this._mode.__creator == modeCreator) { + this._mode.init(this, iv && iv.words); + } else { + this._mode = modeCreator.call(mode, this, iv && iv.words); + this._mode.__creator = modeCreator; + } + }, + + _doProcessBlock: function (words, offset) { + this._mode.processBlock(words, offset); + }, + + _doFinalize: function () { + var finalProcessedBlocks; + + // Shortcut + var padding = this.cfg.padding; + + // Finalize + if (this._xformMode == this._ENC_XFORM_MODE) { + // Pad data + padding.pad(this._data, this.blockSize); + + // Process final blocks + finalProcessedBlocks = this._process(!!'flush'); + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + // Process final blocks + finalProcessedBlocks = this._process(!!'flush'); + + // Unpad data + padding.unpad(finalProcessedBlocks); + } + + return finalProcessedBlocks; + }, + + blockSize: 128/32 + }); + + /** + * A collection of cipher parameters. + * + * @property {WordArray} ciphertext The raw ciphertext. + * @property {WordArray} key The key to this ciphertext. + * @property {WordArray} iv The IV used in the ciphering operation. + * @property {WordArray} salt The salt used with a key derivation function. + * @property {Cipher} algorithm The cipher algorithm. + * @property {Mode} mode The block mode used in the ciphering operation. + * @property {Padding} padding The padding scheme used in the ciphering operation. + * @property {number} blockSize The block size of the cipher. + * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string. + */ + var CipherParams = C_lib.CipherParams = Base.extend({ + /** + * Initializes a newly created cipher params object. + * + * @param {Object} cipherParams An object with any of the possible cipher parameters. + * + * @example + * + * var cipherParams = CryptoJS.lib.CipherParams.create({ + * ciphertext: ciphertextWordArray, + * key: keyWordArray, + * iv: ivWordArray, + * salt: saltWordArray, + * algorithm: CryptoJS.algo.AES, + * mode: CryptoJS.mode.CBC, + * padding: CryptoJS.pad.PKCS7, + * blockSize: 4, + * formatter: CryptoJS.format.OpenSSL + * }); + */ + init: function (cipherParams) { + this.mixIn(cipherParams); + }, + + /** + * Converts this cipher params object to a string. + * + * @param {Format} formatter (Optional) The formatting strategy to use. + * + * @return {string} The stringified cipher params. + * + * @throws Error If neither the formatter nor the default formatter is set. + * + * @example + * + * var string = cipherParams + ''; + * var string = cipherParams.toString(); + * var string = cipherParams.toString(CryptoJS.format.OpenSSL); + */ + toString: function (formatter) { + return (formatter || this.formatter).stringify(this); + } + }); + + /** + * Format namespace. + */ + var C_format = C.format = {}; + + /** + * OpenSSL formatting strategy. + */ + var OpenSSLFormatter = C_format.OpenSSL = { + /** + * Converts a cipher params object to an OpenSSL-compatible string. + * + * @param {CipherParams} cipherParams The cipher params object. + * + * @return {string} The OpenSSL-compatible string. + * + * @static + * + * @example + * + * var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams); + */ + stringify: function (cipherParams) { + var wordArray; + + // Shortcuts + var ciphertext = cipherParams.ciphertext; + var salt = cipherParams.salt; + + // Format + if (salt) { + wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext); + } else { + wordArray = ciphertext; + } + + return wordArray.toString(Base64); + }, + + /** + * Converts an OpenSSL-compatible string to a cipher params object. + * + * @param {string} openSSLStr The OpenSSL-compatible string. + * + * @return {CipherParams} The cipher params object. + * + * @static + * + * @example + * + * var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString); + */ + parse: function (openSSLStr) { + var salt; + + // Parse base64 + var ciphertext = Base64.parse(openSSLStr); + + // Shortcut + var ciphertextWords = ciphertext.words; + + // Test for salt + if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) { + // Extract salt + salt = WordArray.create(ciphertextWords.slice(2, 4)); + + // Remove salt from ciphertext + ciphertextWords.splice(0, 4); + ciphertext.sigBytes -= 16; + } + + return CipherParams.create({ ciphertext: ciphertext, salt: salt }); + } + }; + + /** + * A cipher wrapper that returns ciphertext as a serializable cipher params object. + */ + var SerializableCipher = C_lib.SerializableCipher = Base.extend({ + /** + * Configuration options. + * + * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL + */ + cfg: Base.extend({ + format: OpenSSLFormatter + }), + + /** + * Encrypts a message. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv }); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Encrypt + var encryptor = cipher.createEncryptor(key, cfg); + var ciphertext = encryptor.finalize(message); + + // Shortcut + var cipherCfg = encryptor.cfg; + + // Create and return serializable cipher params + return CipherParams.create({ + ciphertext: ciphertext, + key: key, + iv: cipherCfg.iv, + algorithm: cipher, + mode: cipherCfg.mode, + padding: cipherCfg.padding, + blockSize: cipher.blockSize, + formatter: cfg.format + }); + }, + + /** + * Decrypts serialized ciphertext. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Decrypt + var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext); + + return plaintext; + }, + + /** + * Converts serialized ciphertext to CipherParams, + * else assumed CipherParams already and returns ciphertext unchanged. + * + * @param {CipherParams|string} ciphertext The ciphertext. + * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext. + * + * @return {CipherParams} The unserialized ciphertext. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format); + */ + _parse: function (ciphertext, format) { + if (typeof ciphertext == 'string') { + return format.parse(ciphertext, this); + } else { + return ciphertext; + } + } + }); + + /** + * Key derivation function namespace. + */ + var C_kdf = C.kdf = {}; + + /** + * OpenSSL key derivation function. + */ + var OpenSSLKdf = C_kdf.OpenSSL = { + /** + * Derives a key and IV from a password. + * + * @param {string} password The password to derive from. + * @param {number} keySize The size in words of the key to generate. + * @param {number} ivSize The size in words of the IV to generate. + * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly. + * + * @return {CipherParams} A cipher params object with the key, IV, and salt. + * + * @static + * + * @example + * + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32); + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt'); + */ + execute: function (password, keySize, ivSize, salt) { + // Generate random salt + if (!salt) { + salt = WordArray.random(64/8); + } + + // Derive key and IV + var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt); + + // Separate key and IV + var iv = WordArray.create(key.words.slice(keySize), ivSize * 4); + key.sigBytes = keySize * 4; + + // Return params + return CipherParams.create({ key: key, iv: iv, salt: salt }); + } + }; + + /** + * A serializable cipher wrapper that derives the key from a password, + * and returns ciphertext as a serializable cipher params object. + */ + var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({ + /** + * Configuration options. + * + * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL + */ + cfg: SerializableCipher.cfg.extend({ + kdf: OpenSSLKdf + }), + + /** + * Encrypts a message using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password'); + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Encrypt + var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg); + + // Mix in derived params + ciphertext.mixIn(derivedParams); + + return ciphertext; + }, + + /** + * Decrypts serialized ciphertext using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Decrypt + var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg); + + return plaintext; + } + }); + }()); + + +})); diff --git a/module/static/aes/crypto-js-master/core.js b/module/static/aes/crypto-js-master/core.js new file mode 100644 index 0000000..a1c3625 --- /dev/null +++ b/module/static/aes/crypto-js-master/core.js @@ -0,0 +1,807 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(); + } + else if (typeof define === "function" && define.amd) { + // AMD + define([], factory); + } + else { + // Global (browser) + root.CryptoJS = factory(); + } +}(this, function () { + + /*globals window, global, require*/ + + /** + * CryptoJS core components. + */ + var CryptoJS = CryptoJS || (function (Math, undefined) { + + var crypto; + + // Native crypto from window (Browser) + if (typeof window !== 'undefined' && window.crypto) { + crypto = window.crypto; + } + + // Native crypto in web worker (Browser) + if (typeof self !== 'undefined' && self.crypto) { + crypto = self.crypto; + } + + // Native crypto from worker + if (typeof globalThis !== 'undefined' && globalThis.crypto) { + crypto = globalThis.crypto; + } + + // Native (experimental IE 11) crypto from window (Browser) + if (!crypto && typeof window !== 'undefined' && window.msCrypto) { + crypto = window.msCrypto; + } + + // Native crypto from global (NodeJS) + if (!crypto && typeof global !== 'undefined' && global.crypto) { + crypto = global.crypto; + } + + // Native crypto import via require (NodeJS) + if (!crypto && typeof require === 'function') { + try { + crypto = require('crypto'); + } catch (err) {} + } + + /* + * Cryptographically secure pseudorandom number generator + * + * As Math.random() is cryptographically not safe to use + */ + var cryptoSecureRandomInt = function () { + if (crypto) { + // Use getRandomValues method (Browser) + if (typeof crypto.getRandomValues === 'function') { + try { + return crypto.getRandomValues(new Uint32Array(1))[0]; + } catch (err) {} + } + + // Use randomBytes method (NodeJS) + if (typeof crypto.randomBytes === 'function') { + try { + return crypto.randomBytes(4).readInt32LE(); + } catch (err) {} + } + } + + throw new Error('Native crypto module could not be used to get secure random number.'); + }; + + /* + * Local polyfill of Object.create + + */ + var create = Object.create || (function () { + function F() {} + + return function (obj) { + var subtype; + + F.prototype = obj; + + subtype = new F(); + + F.prototype = null; + + return subtype; + }; + }()); + + /** + * CryptoJS namespace. + */ + var C = {}; + + /** + * Library namespace. + */ + var C_lib = C.lib = {}; + + /** + * Base object for prototypal inheritance. + */ + var Base = C_lib.Base = (function () { + + + return { + /** + * Creates a new object that inherits from this object. + * + * @param {Object} overrides Properties to copy into the new object. + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * field: 'value', + * + * method: function () { + * } + * }); + */ + extend: function (overrides) { + // Spawn + var subtype = create(this); + + // Augment + if (overrides) { + subtype.mixIn(overrides); + } + + // Create default initializer + if (!subtype.hasOwnProperty('init') || this.init === subtype.init) { + subtype.init = function () { + subtype.$super.init.apply(this, arguments); + }; + } + + // Initializer's prototype is the subtype object + subtype.init.prototype = subtype; + + // Reference supertype + subtype.$super = this; + + return subtype; + }, + + /** + * Extends this object and runs the init method. + * Arguments to create() will be passed to init(). + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var instance = MyType.create(); + */ + create: function () { + var instance = this.extend(); + instance.init.apply(instance, arguments); + + return instance; + }, + + /** + * Initializes a newly created object. + * Override this method to add some logic when your objects are created. + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * init: function () { + * // ... + * } + * }); + */ + init: function () { + }, + + /** + * Copies properties into this object. + * + * @param {Object} properties The properties to mix in. + * + * @example + * + * MyType.mixIn({ + * field: 'value' + * }); + */ + mixIn: function (properties) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + this[propertyName] = properties[propertyName]; + } + } + + // IE won't copy toString using the loop above + if (properties.hasOwnProperty('toString')) { + this.toString = properties.toString; + } + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = instance.clone(); + */ + clone: function () { + return this.init.prototype.extend(this); + } + }; + }()); + + /** + * An array of 32-bit words. + * + * @property {Array} words The array of 32-bit words. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var WordArray = C_lib.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of 32-bit words. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.create(); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 4; + } + }, + + /** + * Converts this word array to a string. + * + * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex + * + * @return {string} The stringified word array. + * + * @example + * + * var string = wordArray + ''; + * var string = wordArray.toString(); + * var string = wordArray.toString(CryptoJS.enc.Utf8); + */ + toString: function (encoder) { + return (encoder || Hex).stringify(this); + }, + + /** + * Concatenates a word array to this word array. + * + * @param {WordArray} wordArray The word array to append. + * + * @return {WordArray} This word array. + * + * @example + * + * wordArray1.concat(wordArray2); + */ + concat: function (wordArray) { + // Shortcuts + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + + // Clamp excess bits + this.clamp(); + + // Concat + if (thisSigBytes % 4) { + // Copy one byte at a time + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else { + // Copy one word at a time + for (var j = 0; j < thatSigBytes; j += 4) { + thisWords[(thisSigBytes + j) >>> 2] = thatWords[j >>> 2]; + } + } + this.sigBytes += thatSigBytes; + + // Chainable + return this; + }, + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp: function () { + // Shortcuts + var words = this.words; + var sigBytes = this.sigBytes; + + // Clamp + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, + + /** + * Creates a copy of this word array. + * + * @return {WordArray} The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + + return clone; + }, + + /** + * Creates a word array filled with random bytes. + * + * @param {number} nBytes The number of random bytes to generate. + * + * @return {WordArray} The random word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random: function (nBytes) { + var words = []; + + for (var i = 0; i < nBytes; i += 4) { + words.push(cryptoSecureRandomInt()); + } + + return new WordArray.init(words, nBytes); + } + }); + + /** + * Encoder namespace. + */ + var C_enc = C.enc = {}; + + /** + * Hex encoding strategy. + */ + var Hex = C_enc.Hex = { + /** + * Converts a word array to a hex string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The hex string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + + return hexChars.join(''); + }, + + /** + * Converts a hex string to a word array. + * + * @param {string} hexStr The hex string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse: function (hexStr) { + // Shortcut + var hexStrLength = hexStr.length; + + // Convert + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + + return new WordArray.init(words, hexStrLength / 2); + } + }; + + /** + * Latin1 encoding strategy. + */ + var Latin1 = C_enc.Latin1 = { + /** + * Converts a word array to a Latin1 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Latin1 string. + * + * @static + * + * @example + * + * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + + return latin1Chars.join(''); + }, + + /** + * Converts a Latin1 string to a word array. + * + * @param {string} latin1Str The Latin1 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); + */ + parse: function (latin1Str) { + // Shortcut + var latin1StrLength = latin1Str.length; + + // Convert + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + + return new WordArray.init(words, latin1StrLength); + } + }; + + /** + * UTF-8 encoding strategy. + */ + var Utf8 = C_enc.Utf8 = { + /** + * Converts a word array to a UTF-8 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-8 string. + * + * @static + * + * @example + * + * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); + */ + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + + /** + * Converts a UTF-8 string to a word array. + * + * @param {string} utf8Str The UTF-8 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); + */ + parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + + /** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + * + * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset: function () { + // Initial values + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, + + /** + * Adds new data to this block algorithm's buffer. + * + * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append: function (data) { + // Convert string to WordArray, else assume WordArray already + if (typeof data == 'string') { + data = Utf8.parse(data); + } + + // Append + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, + + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. + * + * @return {WordArray} The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process: function (doFlush) { + var processedWords; + + // Shortcuts + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + + // Count blocks ready + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + // Round up to include partial blocks + nBlocksReady = Math.ceil(nBlocksReady); + } else { + // Round down to include only full blocks, + // less the number of blocks that must remain in the buffer + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + + // Count words ready + var nWordsReady = nBlocksReady * blockSize; + + // Count bytes ready + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + + // Process blocks + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + // Perform concrete-algorithm logic + this._doProcessBlock(dataWords, offset); + } + + // Remove processed words + processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + + // Return processed words + return new WordArray.init(processedWords, nBytesReady); + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + + return clone; + }, + + _minBufferSize: 0 + }); + + /** + * Abstract hasher template. + * + * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + */ + cfg: Base.extend(), + + /** + * Initializes a newly created hasher. + * + * @param {Object} cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + init: function (cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Set initial values + this.reset(); + }, + + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-hasher logic + this._doReset(); + }, + + /** + * Updates this hasher with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {Hasher} This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update: function (messageUpdate) { + // Append + this._append(messageUpdate); + + // Update the hash + this._process(); + + // Chainable + return this; + }, + + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Final message update + if (messageUpdate) { + this._append(messageUpdate); + } + + // Perform concrete-hasher logic + var hash = this._doFinalize(); + + return hash; + }, + + blockSize: 512/32, + + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param {Hasher} hasher The hasher to create a helper for. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, + + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param {Hasher} hasher The hasher to use in this HMAC helper. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + + /** + * Algorithm namespace. + */ + var C_algo = C.algo = {}; + + return C; + }(Math)); + + + return CryptoJS; + +})); diff --git a/module/static/aes/crypto-js-master/crypto-js.js b/module/static/aes/crypto-js-master/crypto-js.js new file mode 100644 index 0000000..70fe18c --- /dev/null +++ b/module/static/aes/crypto-js-master/crypto-js.js @@ -0,0 +1,6191 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(); + } + else if (typeof define === "function" && define.amd) { + // AMD + define([], factory); + } + else { + // Global (browser) + root.CryptoJS = factory(); + } +}(this, function () { + + /*globals window, global, require*/ + + /** + * CryptoJS core components. + */ + var CryptoJS = CryptoJS || (function (Math, undefined) { + + var crypto; + + // Native crypto from window (Browser) + if (typeof window !== 'undefined' && window.crypto) { + crypto = window.crypto; + } + + // Native crypto in web worker (Browser) + if (typeof self !== 'undefined' && self.crypto) { + crypto = self.crypto; + } + + // Native crypto from worker + if (typeof globalThis !== 'undefined' && globalThis.crypto) { + crypto = globalThis.crypto; + } + + // Native (experimental IE 11) crypto from window (Browser) + if (!crypto && typeof window !== 'undefined' && window.msCrypto) { + crypto = window.msCrypto; + } + + // Native crypto from global (NodeJS) + if (!crypto && typeof global !== 'undefined' && global.crypto) { + crypto = global.crypto; + } + + // Native crypto import via require (NodeJS) + if (!crypto && typeof require === 'function') { + try { + crypto = require('crypto'); + } catch (err) {} + } + + /* + * Cryptographically secure pseudorandom number generator + * + * As Math.random() is cryptographically not safe to use + */ + var cryptoSecureRandomInt = function () { + if (crypto) { + // Use getRandomValues method (Browser) + if (typeof crypto.getRandomValues === 'function') { + try { + return crypto.getRandomValues(new Uint32Array(1))[0]; + } catch (err) {} + } + + // Use randomBytes method (NodeJS) + if (typeof crypto.randomBytes === 'function') { + try { + return crypto.randomBytes(4).readInt32LE(); + } catch (err) {} + } + } + + throw new Error('Native crypto module could not be used to get secure random number.'); + }; + + /* + * Local polyfill of Object.create + + */ + var create = Object.create || (function () { + function F() {} + + return function (obj) { + var subtype; + + F.prototype = obj; + + subtype = new F(); + + F.prototype = null; + + return subtype; + }; + }()); + + /** + * CryptoJS namespace. + */ + var C = {}; + + /** + * Library namespace. + */ + var C_lib = C.lib = {}; + + /** + * Base object for prototypal inheritance. + */ + var Base = C_lib.Base = (function () { + + + return { + /** + * Creates a new object that inherits from this object. + * + * @param {Object} overrides Properties to copy into the new object. + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * field: 'value', + * + * method: function () { + * } + * }); + */ + extend: function (overrides) { + // Spawn + var subtype = create(this); + + // Augment + if (overrides) { + subtype.mixIn(overrides); + } + + // Create default initializer + if (!subtype.hasOwnProperty('init') || this.init === subtype.init) { + subtype.init = function () { + subtype.$super.init.apply(this, arguments); + }; + } + + // Initializer's prototype is the subtype object + subtype.init.prototype = subtype; + + // Reference supertype + subtype.$super = this; + + return subtype; + }, + + /** + * Extends this object and runs the init method. + * Arguments to create() will be passed to init(). + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var instance = MyType.create(); + */ + create: function () { + var instance = this.extend(); + instance.init.apply(instance, arguments); + + return instance; + }, + + /** + * Initializes a newly created object. + * Override this method to add some logic when your objects are created. + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * init: function () { + * // ... + * } + * }); + */ + init: function () { + }, + + /** + * Copies properties into this object. + * + * @param {Object} properties The properties to mix in. + * + * @example + * + * MyType.mixIn({ + * field: 'value' + * }); + */ + mixIn: function (properties) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + this[propertyName] = properties[propertyName]; + } + } + + // IE won't copy toString using the loop above + if (properties.hasOwnProperty('toString')) { + this.toString = properties.toString; + } + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = instance.clone(); + */ + clone: function () { + return this.init.prototype.extend(this); + } + }; + }()); + + /** + * An array of 32-bit words. + * + * @property {Array} words The array of 32-bit words. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var WordArray = C_lib.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of 32-bit words. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.create(); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 4; + } + }, + + /** + * Converts this word array to a string. + * + * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex + * + * @return {string} The stringified word array. + * + * @example + * + * var string = wordArray + ''; + * var string = wordArray.toString(); + * var string = wordArray.toString(CryptoJS.enc.Utf8); + */ + toString: function (encoder) { + return (encoder || Hex).stringify(this); + }, + + /** + * Concatenates a word array to this word array. + * + * @param {WordArray} wordArray The word array to append. + * + * @return {WordArray} This word array. + * + * @example + * + * wordArray1.concat(wordArray2); + */ + concat: function (wordArray) { + // Shortcuts + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + + // Clamp excess bits + this.clamp(); + + // Concat + if (thisSigBytes % 4) { + // Copy one byte at a time + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else { + // Copy one word at a time + for (var j = 0; j < thatSigBytes; j += 4) { + thisWords[(thisSigBytes + j) >>> 2] = thatWords[j >>> 2]; + } + } + this.sigBytes += thatSigBytes; + + // Chainable + return this; + }, + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp: function () { + // Shortcuts + var words = this.words; + var sigBytes = this.sigBytes; + + // Clamp + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, + + /** + * Creates a copy of this word array. + * + * @return {WordArray} The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + + return clone; + }, + + /** + * Creates a word array filled with random bytes. + * + * @param {number} nBytes The number of random bytes to generate. + * + * @return {WordArray} The random word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random: function (nBytes) { + var words = []; + + for (var i = 0; i < nBytes; i += 4) { + words.push(cryptoSecureRandomInt()); + } + + return new WordArray.init(words, nBytes); + } + }); + + /** + * Encoder namespace. + */ + var C_enc = C.enc = {}; + + /** + * Hex encoding strategy. + */ + var Hex = C_enc.Hex = { + /** + * Converts a word array to a hex string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The hex string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + + return hexChars.join(''); + }, + + /** + * Converts a hex string to a word array. + * + * @param {string} hexStr The hex string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse: function (hexStr) { + // Shortcut + var hexStrLength = hexStr.length; + + // Convert + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + + return new WordArray.init(words, hexStrLength / 2); + } + }; + + /** + * Latin1 encoding strategy. + */ + var Latin1 = C_enc.Latin1 = { + /** + * Converts a word array to a Latin1 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Latin1 string. + * + * @static + * + * @example + * + * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + + return latin1Chars.join(''); + }, + + /** + * Converts a Latin1 string to a word array. + * + * @param {string} latin1Str The Latin1 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); + */ + parse: function (latin1Str) { + // Shortcut + var latin1StrLength = latin1Str.length; + + // Convert + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + + return new WordArray.init(words, latin1StrLength); + } + }; + + /** + * UTF-8 encoding strategy. + */ + var Utf8 = C_enc.Utf8 = { + /** + * Converts a word array to a UTF-8 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-8 string. + * + * @static + * + * @example + * + * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); + */ + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + + /** + * Converts a UTF-8 string to a word array. + * + * @param {string} utf8Str The UTF-8 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); + */ + parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + + /** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + * + * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset: function () { + // Initial values + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, + + /** + * Adds new data to this block algorithm's buffer. + * + * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append: function (data) { + // Convert string to WordArray, else assume WordArray already + if (typeof data == 'string') { + data = Utf8.parse(data); + } + + // Append + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, + + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. + * + * @return {WordArray} The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process: function (doFlush) { + var processedWords; + + // Shortcuts + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + + // Count blocks ready + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + // Round up to include partial blocks + nBlocksReady = Math.ceil(nBlocksReady); + } else { + // Round down to include only full blocks, + // less the number of blocks that must remain in the buffer + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + + // Count words ready + var nWordsReady = nBlocksReady * blockSize; + + // Count bytes ready + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + + // Process blocks + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + // Perform concrete-algorithm logic + this._doProcessBlock(dataWords, offset); + } + + // Remove processed words + processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + + // Return processed words + return new WordArray.init(processedWords, nBytesReady); + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + + return clone; + }, + + _minBufferSize: 0 + }); + + /** + * Abstract hasher template. + * + * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + */ + cfg: Base.extend(), + + /** + * Initializes a newly created hasher. + * + * @param {Object} cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + init: function (cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Set initial values + this.reset(); + }, + + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-hasher logic + this._doReset(); + }, + + /** + * Updates this hasher with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {Hasher} This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update: function (messageUpdate) { + // Append + this._append(messageUpdate); + + // Update the hash + this._process(); + + // Chainable + return this; + }, + + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Final message update + if (messageUpdate) { + this._append(messageUpdate); + } + + // Perform concrete-hasher logic + var hash = this._doFinalize(); + + return hash; + }, + + blockSize: 512/32, + + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param {Hasher} hasher The hasher to create a helper for. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, + + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param {Hasher} hasher The hasher to use in this HMAC helper. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + + /** + * Algorithm namespace. + */ + var C_algo = C.algo = {}; + + return C; + }(Math)); + + + (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var X32WordArray = C_lib.WordArray; + + /** + * x64 namespace. + */ + var C_x64 = C.x64 = {}; + + /** + * A 64-bit word. + */ + var X64Word = C_x64.Word = Base.extend({ + /** + * Initializes a newly created 64-bit word. + * + * @param {number} high The high 32 bits. + * @param {number} low The low 32 bits. + * + * @example + * + * var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607); + */ + init: function (high, low) { + this.high = high; + this.low = low; + } + + /** + * Bitwise NOTs this word. + * + * @return {X64Word} A new x64-Word object after negating. + * + * @example + * + * var negated = x64Word.not(); + */ + // not: function () { + // var high = ~this.high; + // var low = ~this.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ANDs this word with the passed word. + * + * @param {X64Word} word The x64-Word to AND with this word. + * + * @return {X64Word} A new x64-Word object after ANDing. + * + * @example + * + * var anded = x64Word.and(anotherX64Word); + */ + // and: function (word) { + // var high = this.high & word.high; + // var low = this.low & word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to OR with this word. + * + * @return {X64Word} A new x64-Word object after ORing. + * + * @example + * + * var ored = x64Word.or(anotherX64Word); + */ + // or: function (word) { + // var high = this.high | word.high; + // var low = this.low | word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise XORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to XOR with this word. + * + * @return {X64Word} A new x64-Word object after XORing. + * + * @example + * + * var xored = x64Word.xor(anotherX64Word); + */ + // xor: function (word) { + // var high = this.high ^ word.high; + // var low = this.low ^ word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the left. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftL(25); + */ + // shiftL: function (n) { + // if (n < 32) { + // var high = (this.high << n) | (this.low >>> (32 - n)); + // var low = this.low << n; + // } else { + // var high = this.low << (n - 32); + // var low = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the right. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftR(7); + */ + // shiftR: function (n) { + // if (n < 32) { + // var low = (this.low >>> n) | (this.high << (32 - n)); + // var high = this.high >>> n; + // } else { + // var low = this.high >>> (n - 32); + // var high = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Rotates this word n bits to the left. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotL(25); + */ + // rotL: function (n) { + // return this.shiftL(n).or(this.shiftR(64 - n)); + // }, + + /** + * Rotates this word n bits to the right. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotR(7); + */ + // rotR: function (n) { + // return this.shiftR(n).or(this.shiftL(64 - n)); + // }, + + /** + * Adds this word with the passed word. + * + * @param {X64Word} word The x64-Word to add with this word. + * + * @return {X64Word} A new x64-Word object after adding. + * + * @example + * + * var added = x64Word.add(anotherX64Word); + */ + // add: function (word) { + // var low = (this.low + word.low) | 0; + // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0; + // var high = (this.high + word.high + carry) | 0; + + // return X64Word.create(high, low); + // } + }); + + /** + * An array of 64-bit words. + * + * @property {Array} words The array of CryptoJS.x64.Word objects. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var X64WordArray = C_x64.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.x64.WordArray.create(); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ]); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ], 10); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 8; + } + }, + + /** + * Converts this 64-bit word array to a 32-bit word array. + * + * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array. + * + * @example + * + * var x32WordArray = x64WordArray.toX32(); + */ + toX32: function () { + // Shortcuts + var x64Words = this.words; + var x64WordsLength = x64Words.length; + + // Convert + var x32Words = []; + for (var i = 0; i < x64WordsLength; i++) { + var x64Word = x64Words[i]; + x32Words.push(x64Word.high); + x32Words.push(x64Word.low); + } + + return X32WordArray.create(x32Words, this.sigBytes); + }, + + /** + * Creates a copy of this word array. + * + * @return {X64WordArray} The clone. + * + * @example + * + * var clone = x64WordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + + // Clone "words" array + var words = clone.words = this.words.slice(0); + + // Clone each X64Word object + var wordsLength = words.length; + for (var i = 0; i < wordsLength; i++) { + words[i] = words[i].clone(); + } + + return clone; + } + }); + }()); + + + (function () { + // Check if typed arrays are supported + if (typeof ArrayBuffer != 'function') { + return; + } + + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + + // Reference original init + var superInit = WordArray.init; + + // Augment WordArray.init to handle typed arrays + var subInit = WordArray.init = function (typedArray) { + // Convert buffers to uint8 + if (typedArray instanceof ArrayBuffer) { + typedArray = new Uint8Array(typedArray); + } + + // Convert other array views to uint8 + if ( + typedArray instanceof Int8Array || + (typeof Uint8ClampedArray !== "undefined" && typedArray instanceof Uint8ClampedArray) || + typedArray instanceof Int16Array || + typedArray instanceof Uint16Array || + typedArray instanceof Int32Array || + typedArray instanceof Uint32Array || + typedArray instanceof Float32Array || + typedArray instanceof Float64Array + ) { + typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); + } + + // Handle Uint8Array + if (typedArray instanceof Uint8Array) { + // Shortcut + var typedArrayByteLength = typedArray.byteLength; + + // Extract bytes + var words = []; + for (var i = 0; i < typedArrayByteLength; i++) { + words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8); + } + + // Initialize this word array + superInit.call(this, words, typedArrayByteLength); + } else { + // Else call normal init + superInit.apply(this, arguments); + } + }; + + subInit.prototype = WordArray; + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * UTF-16 BE encoding strategy. + */ + var Utf16BE = C_enc.Utf16 = C_enc.Utf16BE = { + /** + * Converts a word array to a UTF-16 BE string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-16 BE string. + * + * @static + * + * @example + * + * var utf16String = CryptoJS.enc.Utf16.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var utf16Chars = []; + for (var i = 0; i < sigBytes; i += 2) { + var codePoint = (words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff; + utf16Chars.push(String.fromCharCode(codePoint)); + } + + return utf16Chars.join(''); + }, + + /** + * Converts a UTF-16 BE string to a word array. + * + * @param {string} utf16Str The UTF-16 BE string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf16.parse(utf16String); + */ + parse: function (utf16Str) { + // Shortcut + var utf16StrLength = utf16Str.length; + + // Convert + var words = []; + for (var i = 0; i < utf16StrLength; i++) { + words[i >>> 1] |= utf16Str.charCodeAt(i) << (16 - (i % 2) * 16); + } + + return WordArray.create(words, utf16StrLength * 2); + } + }; + + /** + * UTF-16 LE encoding strategy. + */ + C_enc.Utf16LE = { + /** + * Converts a word array to a UTF-16 LE string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-16 LE string. + * + * @static + * + * @example + * + * var utf16Str = CryptoJS.enc.Utf16LE.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var utf16Chars = []; + for (var i = 0; i < sigBytes; i += 2) { + var codePoint = swapEndian((words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff); + utf16Chars.push(String.fromCharCode(codePoint)); + } + + return utf16Chars.join(''); + }, + + /** + * Converts a UTF-16 LE string to a word array. + * + * @param {string} utf16Str The UTF-16 LE string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf16LE.parse(utf16Str); + */ + parse: function (utf16Str) { + // Shortcut + var utf16StrLength = utf16Str.length; + + // Convert + var words = []; + for (var i = 0; i < utf16StrLength; i++) { + words[i >>> 1] |= swapEndian(utf16Str.charCodeAt(i) << (16 - (i % 2) * 16)); + } + + return WordArray.create(words, utf16StrLength * 2); + } + }; + + function swapEndian(word) { + return ((word << 8) & 0xff00ff00) | ((word >>> 8) & 0x00ff00ff); + } + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * Base64 encoding strategy. + */ + var Base64 = C_enc.Base64 = { + /** + * Converts a word array to a Base64 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Base64 string. + * + * @static + * + * @example + * + * var base64String = CryptoJS.enc.Base64.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var map = this._map; + + // Clamp excess bits + wordArray.clamp(); + + // Convert + var base64Chars = []; + for (var i = 0; i < sigBytes; i += 3) { + var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + + var triplet = (byte1 << 16) | (byte2 << 8) | byte3; + + for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { + base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); + } + } + + // Add padding + var paddingChar = map.charAt(64); + if (paddingChar) { + while (base64Chars.length % 4) { + base64Chars.push(paddingChar); + } + } + + return base64Chars.join(''); + }, + + /** + * Converts a Base64 string to a word array. + * + * @param {string} base64Str The Base64 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Base64.parse(base64String); + */ + parse: function (base64Str) { + // Shortcuts + var base64StrLength = base64Str.length; + var map = this._map; + var reverseMap = this._reverseMap; + + if (!reverseMap) { + reverseMap = this._reverseMap = []; + for (var j = 0; j < map.length; j++) { + reverseMap[map.charCodeAt(j)] = j; + } + } + + // Ignore padding + var paddingChar = map.charAt(64); + if (paddingChar) { + var paddingIndex = base64Str.indexOf(paddingChar); + if (paddingIndex !== -1) { + base64StrLength = paddingIndex; + } + } + + // Convert + return parseLoop(base64Str, base64StrLength, reverseMap); + + }, + + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + }; + + function parseLoop(base64Str, base64StrLength, reverseMap) { + var words = []; + var nBytes = 0; + for (var i = 0; i < base64StrLength; i++) { + if (i % 4) { + var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); + var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); + var bitsCombined = bits1 | bits2; + words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); + nBytes++; + } + } + return WordArray.create(words, nBytes); + } + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * Base64url encoding strategy. + */ + var Base64url = C_enc.Base64url = { + /** + * Converts a word array to a Base64url string. + * + * @param {WordArray} wordArray The word array. + * + * @param {boolean} urlSafe Whether to use url safe + * + * @return {string} The Base64url string. + * + * @static + * + * @example + * + * var base64String = CryptoJS.enc.Base64url.stringify(wordArray); + */ + stringify: function (wordArray, urlSafe=true) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var map = urlSafe ? this._safe_map : this._map; + + // Clamp excess bits + wordArray.clamp(); + + // Convert + var base64Chars = []; + for (var i = 0; i < sigBytes; i += 3) { + var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + + var triplet = (byte1 << 16) | (byte2 << 8) | byte3; + + for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { + base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); + } + } + + // Add padding + var paddingChar = map.charAt(64); + if (paddingChar) { + while (base64Chars.length % 4) { + base64Chars.push(paddingChar); + } + } + + return base64Chars.join(''); + }, + + /** + * Converts a Base64url string to a word array. + * + * @param {string} base64Str The Base64url string. + * + * @param {boolean} urlSafe Whether to use url safe + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Base64url.parse(base64String); + */ + parse: function (base64Str, urlSafe=true) { + // Shortcuts + var base64StrLength = base64Str.length; + var map = urlSafe ? this._safe_map : this._map; + var reverseMap = this._reverseMap; + + if (!reverseMap) { + reverseMap = this._reverseMap = []; + for (var j = 0; j < map.length; j++) { + reverseMap[map.charCodeAt(j)] = j; + } + } + + // Ignore padding + var paddingChar = map.charAt(64); + if (paddingChar) { + var paddingIndex = base64Str.indexOf(paddingChar); + if (paddingIndex !== -1) { + base64StrLength = paddingIndex; + } + } + + // Convert + return parseLoop(base64Str, base64StrLength, reverseMap); + + }, + + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', + _safe_map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + }; + + function parseLoop(base64Str, base64StrLength, reverseMap) { + var words = []; + var nBytes = 0; + for (var i = 0; i < base64StrLength; i++) { + if (i % 4) { + var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); + var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); + var bitsCombined = bits1 | bits2; + words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); + nBytes++; + } + } + return WordArray.create(words, nBytes); + } + }()); + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Constants table + var T = []; + + // Compute constants + (function () { + for (var i = 0; i < 64; i++) { + T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0; + } + }()); + + /** + * MD5 hash algorithm. + */ + var MD5 = C_algo.MD5 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Swap endian + for (var i = 0; i < 16; i++) { + // Shortcuts + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + + // Shortcuts + var H = this._hash.words; + + var M_offset_0 = M[offset + 0]; + var M_offset_1 = M[offset + 1]; + var M_offset_2 = M[offset + 2]; + var M_offset_3 = M[offset + 3]; + var M_offset_4 = M[offset + 4]; + var M_offset_5 = M[offset + 5]; + var M_offset_6 = M[offset + 6]; + var M_offset_7 = M[offset + 7]; + var M_offset_8 = M[offset + 8]; + var M_offset_9 = M[offset + 9]; + var M_offset_10 = M[offset + 10]; + var M_offset_11 = M[offset + 11]; + var M_offset_12 = M[offset + 12]; + var M_offset_13 = M[offset + 13]; + var M_offset_14 = M[offset + 14]; + var M_offset_15 = M[offset + 15]; + + // Working varialbes + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + + // Computation + a = FF(a, b, c, d, M_offset_0, 7, T[0]); + d = FF(d, a, b, c, M_offset_1, 12, T[1]); + c = FF(c, d, a, b, M_offset_2, 17, T[2]); + b = FF(b, c, d, a, M_offset_3, 22, T[3]); + a = FF(a, b, c, d, M_offset_4, 7, T[4]); + d = FF(d, a, b, c, M_offset_5, 12, T[5]); + c = FF(c, d, a, b, M_offset_6, 17, T[6]); + b = FF(b, c, d, a, M_offset_7, 22, T[7]); + a = FF(a, b, c, d, M_offset_8, 7, T[8]); + d = FF(d, a, b, c, M_offset_9, 12, T[9]); + c = FF(c, d, a, b, M_offset_10, 17, T[10]); + b = FF(b, c, d, a, M_offset_11, 22, T[11]); + a = FF(a, b, c, d, M_offset_12, 7, T[12]); + d = FF(d, a, b, c, M_offset_13, 12, T[13]); + c = FF(c, d, a, b, M_offset_14, 17, T[14]); + b = FF(b, c, d, a, M_offset_15, 22, T[15]); + + a = GG(a, b, c, d, M_offset_1, 5, T[16]); + d = GG(d, a, b, c, M_offset_6, 9, T[17]); + c = GG(c, d, a, b, M_offset_11, 14, T[18]); + b = GG(b, c, d, a, M_offset_0, 20, T[19]); + a = GG(a, b, c, d, M_offset_5, 5, T[20]); + d = GG(d, a, b, c, M_offset_10, 9, T[21]); + c = GG(c, d, a, b, M_offset_15, 14, T[22]); + b = GG(b, c, d, a, M_offset_4, 20, T[23]); + a = GG(a, b, c, d, M_offset_9, 5, T[24]); + d = GG(d, a, b, c, M_offset_14, 9, T[25]); + c = GG(c, d, a, b, M_offset_3, 14, T[26]); + b = GG(b, c, d, a, M_offset_8, 20, T[27]); + a = GG(a, b, c, d, M_offset_13, 5, T[28]); + d = GG(d, a, b, c, M_offset_2, 9, T[29]); + c = GG(c, d, a, b, M_offset_7, 14, T[30]); + b = GG(b, c, d, a, M_offset_12, 20, T[31]); + + a = HH(a, b, c, d, M_offset_5, 4, T[32]); + d = HH(d, a, b, c, M_offset_8, 11, T[33]); + c = HH(c, d, a, b, M_offset_11, 16, T[34]); + b = HH(b, c, d, a, M_offset_14, 23, T[35]); + a = HH(a, b, c, d, M_offset_1, 4, T[36]); + d = HH(d, a, b, c, M_offset_4, 11, T[37]); + c = HH(c, d, a, b, M_offset_7, 16, T[38]); + b = HH(b, c, d, a, M_offset_10, 23, T[39]); + a = HH(a, b, c, d, M_offset_13, 4, T[40]); + d = HH(d, a, b, c, M_offset_0, 11, T[41]); + c = HH(c, d, a, b, M_offset_3, 16, T[42]); + b = HH(b, c, d, a, M_offset_6, 23, T[43]); + a = HH(a, b, c, d, M_offset_9, 4, T[44]); + d = HH(d, a, b, c, M_offset_12, 11, T[45]); + c = HH(c, d, a, b, M_offset_15, 16, T[46]); + b = HH(b, c, d, a, M_offset_2, 23, T[47]); + + a = II(a, b, c, d, M_offset_0, 6, T[48]); + d = II(d, a, b, c, M_offset_7, 10, T[49]); + c = II(c, d, a, b, M_offset_14, 15, T[50]); + b = II(b, c, d, a, M_offset_5, 21, T[51]); + a = II(a, b, c, d, M_offset_12, 6, T[52]); + d = II(d, a, b, c, M_offset_3, 10, T[53]); + c = II(c, d, a, b, M_offset_10, 15, T[54]); + b = II(b, c, d, a, M_offset_1, 21, T[55]); + a = II(a, b, c, d, M_offset_8, 6, T[56]); + d = II(d, a, b, c, M_offset_15, 10, T[57]); + c = II(c, d, a, b, M_offset_6, 15, T[58]); + b = II(b, c, d, a, M_offset_13, 21, T[59]); + a = II(a, b, c, d, M_offset_4, 6, T[60]); + d = II(d, a, b, c, M_offset_11, 10, T[61]); + c = II(c, d, a, b, M_offset_2, 15, T[62]); + b = II(b, c, d, a, M_offset_9, 21, T[63]); + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + + var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000); + var nBitsTotalL = nBitsTotal; + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ( + (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | + (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00) + ); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | + (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00) + ); + + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 4; i++) { + // Shortcut + var H_i = H[i]; + + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + function FF(a, b, c, d, x, s, t) { + var n = a + ((b & c) | (~b & d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function GG(a, b, c, d, x, s, t) { + var n = a + ((b & d) | (c & ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function HH(a, b, c, d, x, s, t) { + var n = a + (b ^ c ^ d) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function II(a, b, c, d, x, s, t) { + var n = a + (c ^ (b | ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.MD5('message'); + * var hash = CryptoJS.MD5(wordArray); + */ + C.MD5 = Hasher._createHelper(MD5); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacMD5(message, key); + */ + C.HmacMD5 = Hasher._createHmacHelper(MD5); + }(Math)); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Reusable object + var W = []; + + /** + * SHA-1 hash algorithm. + */ + var SHA1 = C_algo.SHA1 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476, + 0xc3d2e1f0 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + + // Computation + for (var i = 0; i < 80; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; + W[i] = (n << 1) | (n >>> 31); + } + + var t = ((a << 5) | (a >>> 27)) + e + W[i]; + if (i < 20) { + t += ((b & c) | (~b & d)) + 0x5a827999; + } else if (i < 40) { + t += (b ^ c ^ d) + 0x6ed9eba1; + } else if (i < 60) { + t += ((b & c) | (b & d) | (c & d)) - 0x70e44324; + } else /* if (i < 80) */ { + t += (b ^ c ^ d) - 0x359d3e2a; + } + + e = d; + d = c; + c = (b << 30) | (b >>> 2); + b = a; + a = t; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA1('message'); + * var hash = CryptoJS.SHA1(wordArray); + */ + C.SHA1 = Hasher._createHelper(SHA1); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA1(message, key); + */ + C.HmacSHA1 = Hasher._createHmacHelper(SHA1); + }()); + + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Initialization and round constants tables + var H = []; + var K = []; + + // Compute constants + (function () { + function isPrime(n) { + var sqrtN = Math.sqrt(n); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n % factor)) { + return false; + } + } + + return true; + } + + function getFractionalBits(n) { + return ((n - (n | 0)) * 0x100000000) | 0; + } + + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); + + nPrime++; + } + + n++; + } + }()); + + // Reusable object + var W = []; + + /** + * SHA-256 hash algorithm. + */ + var SHA256 = C_algo.SHA256 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init(H.slice(0)); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + var f = H[5]; + var g = H[6]; + var h = H[7]; + + // Computation + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ + ((gamma0x << 14) | (gamma0x >>> 18)) ^ + (gamma0x >>> 3); + + var gamma1x = W[i - 2]; + var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ + ((gamma1x << 13) | (gamma1x >>> 19)) ^ + (gamma1x >>> 10); + + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + + var ch = (e & f) ^ (~e & g); + var maj = (a & b) ^ (a & c) ^ (b & c); + + var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); + var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); + + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + + h = g; + g = f; + f = e; + e = (d + t1) | 0; + d = c; + c = b; + b = a; + a = (t1 + t2) | 0; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + H[5] = (H[5] + f) | 0; + H[6] = (H[6] + g) | 0; + H[7] = (H[7] + h) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA256('message'); + * var hash = CryptoJS.SHA256(wordArray); + */ + C.SHA256 = Hasher._createHelper(SHA256); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA256(message, key); + */ + C.HmacSHA256 = Hasher._createHmacHelper(SHA256); + }(Math)); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var SHA256 = C_algo.SHA256; + + /** + * SHA-224 hash algorithm. + */ + var SHA224 = C_algo.SHA224 = SHA256.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, + 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4 + ]); + }, + + _doFinalize: function () { + var hash = SHA256._doFinalize.call(this); + + hash.sigBytes -= 4; + + return hash; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA224('message'); + * var hash = CryptoJS.SHA224(wordArray); + */ + C.SHA224 = SHA256._createHelper(SHA224); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA224(message, key); + */ + C.HmacSHA224 = SHA256._createHmacHelper(SHA224); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Hasher = C_lib.Hasher; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var X64WordArray = C_x64.WordArray; + var C_algo = C.algo; + + function X64Word_create() { + return X64Word.create.apply(X64Word, arguments); + } + + // Constants + var K = [ + X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd), + X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc), + X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019), + X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118), + X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe), + X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2), + X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1), + X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694), + X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3), + X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65), + X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483), + X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5), + X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210), + X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4), + X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725), + X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70), + X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926), + X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df), + X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8), + X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b), + X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001), + X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30), + X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910), + X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8), + X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53), + X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8), + X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb), + X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3), + X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60), + X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec), + X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9), + X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b), + X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207), + X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178), + X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6), + X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b), + X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493), + X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c), + X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a), + X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817) + ]; + + // Reusable objects + var W = []; + (function () { + for (var i = 0; i < 80; i++) { + W[i] = X64Word_create(); + } + }()); + + /** + * SHA-512 hash algorithm. + */ + var SHA512 = C_algo.SHA512 = Hasher.extend({ + _doReset: function () { + this._hash = new X64WordArray.init([ + new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b), + new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1), + new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f), + new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179) + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcuts + var H = this._hash.words; + + var H0 = H[0]; + var H1 = H[1]; + var H2 = H[2]; + var H3 = H[3]; + var H4 = H[4]; + var H5 = H[5]; + var H6 = H[6]; + var H7 = H[7]; + + var H0h = H0.high; + var H0l = H0.low; + var H1h = H1.high; + var H1l = H1.low; + var H2h = H2.high; + var H2l = H2.low; + var H3h = H3.high; + var H3l = H3.low; + var H4h = H4.high; + var H4l = H4.low; + var H5h = H5.high; + var H5l = H5.low; + var H6h = H6.high; + var H6l = H6.low; + var H7h = H7.high; + var H7l = H7.low; + + // Working variables + var ah = H0h; + var al = H0l; + var bh = H1h; + var bl = H1l; + var ch = H2h; + var cl = H2l; + var dh = H3h; + var dl = H3l; + var eh = H4h; + var el = H4l; + var fh = H5h; + var fl = H5l; + var gh = H6h; + var gl = H6l; + var hh = H7h; + var hl = H7l; + + // Rounds + for (var i = 0; i < 80; i++) { + var Wil; + var Wih; + + // Shortcut + var Wi = W[i]; + + // Extend message + if (i < 16) { + Wih = Wi.high = M[offset + i * 2] | 0; + Wil = Wi.low = M[offset + i * 2 + 1] | 0; + } else { + // Gamma0 + var gamma0x = W[i - 15]; + var gamma0xh = gamma0x.high; + var gamma0xl = gamma0x.low; + var gamma0h = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7); + var gamma0l = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25)); + + // Gamma1 + var gamma1x = W[i - 2]; + var gamma1xh = gamma1x.high; + var gamma1xl = gamma1x.low; + var gamma1h = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6); + var gamma1l = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26)); + + // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16] + var Wi7 = W[i - 7]; + var Wi7h = Wi7.high; + var Wi7l = Wi7.low; + + var Wi16 = W[i - 16]; + var Wi16h = Wi16.high; + var Wi16l = Wi16.low; + + Wil = gamma0l + Wi7l; + Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0); + Wil = Wil + gamma1l; + Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0); + Wil = Wil + Wi16l; + Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0); + + Wi.high = Wih; + Wi.low = Wil; + } + + var chh = (eh & fh) ^ (~eh & gh); + var chl = (el & fl) ^ (~el & gl); + var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch); + var majl = (al & bl) ^ (al & cl) ^ (bl & cl); + + var sigma0h = ((ah >>> 28) | (al << 4)) ^ ((ah << 30) | (al >>> 2)) ^ ((ah << 25) | (al >>> 7)); + var sigma0l = ((al >>> 28) | (ah << 4)) ^ ((al << 30) | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7)); + var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9)); + var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9)); + + // t1 = h + sigma1 + ch + K[i] + W[i] + var Ki = K[i]; + var Kih = Ki.high; + var Kil = Ki.low; + + var t1l = hl + sigma1l; + var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0); + var t1l = t1l + chl; + var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0); + var t1l = t1l + Kil; + var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0); + var t1l = t1l + Wil; + var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0); + + // t2 = sigma0 + maj + var t2l = sigma0l + majl; + var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0); + + // Update working variables + hh = gh; + hl = gl; + gh = fh; + gl = fl; + fh = eh; + fl = el; + el = (dl + t1l) | 0; + eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0; + dh = ch; + dl = cl; + ch = bh; + cl = bl; + bh = ah; + bl = al; + al = (t1l + t2l) | 0; + ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0; + } + + // Intermediate hash value + H0l = H0.low = (H0l + al); + H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0)); + H1l = H1.low = (H1l + bl); + H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0)); + H2l = H2.low = (H2l + cl); + H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0)); + H3l = H3.low = (H3l + dl); + H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0)); + H4l = H4.low = (H4l + el); + H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0)); + H5l = H5.low = (H5l + fl); + H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0)); + H6l = H6.low = (H6l + gl); + H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0)); + H7l = H7.low = (H7l + hl); + H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0)); + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Convert hash to 32-bit word array before returning + var hash = this._hash.toX32(); + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + }, + + blockSize: 1024/32 + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA512('message'); + * var hash = CryptoJS.SHA512(wordArray); + */ + C.SHA512 = Hasher._createHelper(SHA512); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA512(message, key); + */ + C.HmacSHA512 = Hasher._createHmacHelper(SHA512); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var X64WordArray = C_x64.WordArray; + var C_algo = C.algo; + var SHA512 = C_algo.SHA512; + + /** + * SHA-384 hash algorithm. + */ + var SHA384 = C_algo.SHA384 = SHA512.extend({ + _doReset: function () { + this._hash = new X64WordArray.init([ + new X64Word.init(0xcbbb9d5d, 0xc1059ed8), new X64Word.init(0x629a292a, 0x367cd507), + new X64Word.init(0x9159015a, 0x3070dd17), new X64Word.init(0x152fecd8, 0xf70e5939), + new X64Word.init(0x67332667, 0xffc00b31), new X64Word.init(0x8eb44a87, 0x68581511), + new X64Word.init(0xdb0c2e0d, 0x64f98fa7), new X64Word.init(0x47b5481d, 0xbefa4fa4) + ]); + }, + + _doFinalize: function () { + var hash = SHA512._doFinalize.call(this); + + hash.sigBytes -= 16; + + return hash; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA384('message'); + * var hash = CryptoJS.SHA384(wordArray); + */ + C.SHA384 = SHA512._createHelper(SHA384); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA384(message, key); + */ + C.HmacSHA384 = SHA512._createHmacHelper(SHA384); + }()); + + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var C_algo = C.algo; + + // Constants tables + var RHO_OFFSETS = []; + var PI_INDEXES = []; + var ROUND_CONSTANTS = []; + + // Compute Constants + (function () { + // Compute rho offset constants + var x = 1, y = 0; + for (var t = 0; t < 24; t++) { + RHO_OFFSETS[x + 5 * y] = ((t + 1) * (t + 2) / 2) % 64; + + var newX = y % 5; + var newY = (2 * x + 3 * y) % 5; + x = newX; + y = newY; + } + + // Compute pi index constants + for (var x = 0; x < 5; x++) { + for (var y = 0; y < 5; y++) { + PI_INDEXES[x + 5 * y] = y + ((2 * x + 3 * y) % 5) * 5; + } + } + + // Compute round constants + var LFSR = 0x01; + for (var i = 0; i < 24; i++) { + var roundConstantMsw = 0; + var roundConstantLsw = 0; + + for (var j = 0; j < 7; j++) { + if (LFSR & 0x01) { + var bitPosition = (1 << j) - 1; + if (bitPosition < 32) { + roundConstantLsw ^= 1 << bitPosition; + } else /* if (bitPosition >= 32) */ { + roundConstantMsw ^= 1 << (bitPosition - 32); + } + } + + // Compute next LFSR + if (LFSR & 0x80) { + // Primitive polynomial over GF(2): x^8 + x^6 + x^5 + x^4 + 1 + LFSR = (LFSR << 1) ^ 0x71; + } else { + LFSR <<= 1; + } + } + + ROUND_CONSTANTS[i] = X64Word.create(roundConstantMsw, roundConstantLsw); + } + }()); + + // Reusable objects for temporary values + var T = []; + (function () { + for (var i = 0; i < 25; i++) { + T[i] = X64Word.create(); + } + }()); + + /** + * SHA-3 hash algorithm. + */ + var SHA3 = C_algo.SHA3 = Hasher.extend({ + /** + * Configuration options. + * + * @property {number} outputLength + * The desired number of bits in the output hash. + * Only values permitted are: 224, 256, 384, 512. + * Default: 512 + */ + cfg: Hasher.cfg.extend({ + outputLength: 512 + }), + + _doReset: function () { + var state = this._state = [] + for (var i = 0; i < 25; i++) { + state[i] = new X64Word.init(); + } + + this.blockSize = (1600 - 2 * this.cfg.outputLength) / 32; + }, + + _doProcessBlock: function (M, offset) { + // Shortcuts + var state = this._state; + var nBlockSizeLanes = this.blockSize / 2; + + // Absorb + for (var i = 0; i < nBlockSizeLanes; i++) { + // Shortcuts + var M2i = M[offset + 2 * i]; + var M2i1 = M[offset + 2 * i + 1]; + + // Swap endian + M2i = ( + (((M2i << 8) | (M2i >>> 24)) & 0x00ff00ff) | + (((M2i << 24) | (M2i >>> 8)) & 0xff00ff00) + ); + M2i1 = ( + (((M2i1 << 8) | (M2i1 >>> 24)) & 0x00ff00ff) | + (((M2i1 << 24) | (M2i1 >>> 8)) & 0xff00ff00) + ); + + // Absorb message into state + var lane = state[i]; + lane.high ^= M2i1; + lane.low ^= M2i; + } + + // Rounds + for (var round = 0; round < 24; round++) { + // Theta + for (var x = 0; x < 5; x++) { + // Mix column lanes + var tMsw = 0, tLsw = 0; + for (var y = 0; y < 5; y++) { + var lane = state[x + 5 * y]; + tMsw ^= lane.high; + tLsw ^= lane.low; + } + + // Temporary values + var Tx = T[x]; + Tx.high = tMsw; + Tx.low = tLsw; + } + for (var x = 0; x < 5; x++) { + // Shortcuts + var Tx4 = T[(x + 4) % 5]; + var Tx1 = T[(x + 1) % 5]; + var Tx1Msw = Tx1.high; + var Tx1Lsw = Tx1.low; + + // Mix surrounding columns + var tMsw = Tx4.high ^ ((Tx1Msw << 1) | (Tx1Lsw >>> 31)); + var tLsw = Tx4.low ^ ((Tx1Lsw << 1) | (Tx1Msw >>> 31)); + for (var y = 0; y < 5; y++) { + var lane = state[x + 5 * y]; + lane.high ^= tMsw; + lane.low ^= tLsw; + } + } + + // Rho Pi + for (var laneIndex = 1; laneIndex < 25; laneIndex++) { + var tMsw; + var tLsw; + + // Shortcuts + var lane = state[laneIndex]; + var laneMsw = lane.high; + var laneLsw = lane.low; + var rhoOffset = RHO_OFFSETS[laneIndex]; + + // Rotate lanes + if (rhoOffset < 32) { + tMsw = (laneMsw << rhoOffset) | (laneLsw >>> (32 - rhoOffset)); + tLsw = (laneLsw << rhoOffset) | (laneMsw >>> (32 - rhoOffset)); + } else /* if (rhoOffset >= 32) */ { + tMsw = (laneLsw << (rhoOffset - 32)) | (laneMsw >>> (64 - rhoOffset)); + tLsw = (laneMsw << (rhoOffset - 32)) | (laneLsw >>> (64 - rhoOffset)); + } + + // Transpose lanes + var TPiLane = T[PI_INDEXES[laneIndex]]; + TPiLane.high = tMsw; + TPiLane.low = tLsw; + } + + // Rho pi at x = y = 0 + var T0 = T[0]; + var state0 = state[0]; + T0.high = state0.high; + T0.low = state0.low; + + // Chi + for (var x = 0; x < 5; x++) { + for (var y = 0; y < 5; y++) { + // Shortcuts + var laneIndex = x + 5 * y; + var lane = state[laneIndex]; + var TLane = T[laneIndex]; + var Tx1Lane = T[((x + 1) % 5) + 5 * y]; + var Tx2Lane = T[((x + 2) % 5) + 5 * y]; + + // Mix rows + lane.high = TLane.high ^ (~Tx1Lane.high & Tx2Lane.high); + lane.low = TLane.low ^ (~Tx1Lane.low & Tx2Lane.low); + } + } + + // Iota + var lane = state[0]; + var roundConstant = ROUND_CONSTANTS[round]; + lane.high ^= roundConstant.high; + lane.low ^= roundConstant.low; + } + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + var blockSizeBits = this.blockSize * 32; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x1 << (24 - nBitsLeft % 32); + dataWords[((Math.ceil((nBitsLeft + 1) / blockSizeBits) * blockSizeBits) >>> 5) - 1] |= 0x80; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var state = this._state; + var outputLengthBytes = this.cfg.outputLength / 8; + var outputLengthLanes = outputLengthBytes / 8; + + // Squeeze + var hashWords = []; + for (var i = 0; i < outputLengthLanes; i++) { + // Shortcuts + var lane = state[i]; + var laneMsw = lane.high; + var laneLsw = lane.low; + + // Swap endian + laneMsw = ( + (((laneMsw << 8) | (laneMsw >>> 24)) & 0x00ff00ff) | + (((laneMsw << 24) | (laneMsw >>> 8)) & 0xff00ff00) + ); + laneLsw = ( + (((laneLsw << 8) | (laneLsw >>> 24)) & 0x00ff00ff) | + (((laneLsw << 24) | (laneLsw >>> 8)) & 0xff00ff00) + ); + + // Squeeze state to retrieve hash + hashWords.push(laneLsw); + hashWords.push(laneMsw); + } + + // Return final computed hash + return new WordArray.init(hashWords, outputLengthBytes); + }, + + clone: function () { + var clone = Hasher.clone.call(this); + + var state = clone._state = this._state.slice(0); + for (var i = 0; i < 25; i++) { + state[i] = state[i].clone(); + } + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA3('message'); + * var hash = CryptoJS.SHA3(wordArray); + */ + C.SHA3 = Hasher._createHelper(SHA3); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA3(message, key); + */ + C.HmacSHA3 = Hasher._createHmacHelper(SHA3); + }(Math)); + + + /** @preserve + (c) 2012 by Cédric Mesnil. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Constants table + var _zl = WordArray.create([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13]); + var _zr = WordArray.create([ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11]); + var _sl = WordArray.create([ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ]); + var _sr = WordArray.create([ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ]); + + var _hl = WordArray.create([ 0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]); + var _hr = WordArray.create([ 0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000]); + + /** + * RIPEMD160 hash algorithm. + */ + var RIPEMD160 = C_algo.RIPEMD160 = Hasher.extend({ + _doReset: function () { + this._hash = WordArray.create([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]); + }, + + _doProcessBlock: function (M, offset) { + + // Swap endian + for (var i = 0; i < 16; i++) { + // Shortcuts + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + // Swap + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + // Shortcut + var H = this._hash.words; + var hl = _hl.words; + var hr = _hr.words; + var zl = _zl.words; + var zr = _zr.words; + var sl = _sl.words; + var sr = _sr.words; + + // Working variables + var al, bl, cl, dl, el; + var ar, br, cr, dr, er; + + ar = al = H[0]; + br = bl = H[1]; + cr = cl = H[2]; + dr = dl = H[3]; + er = el = H[4]; + // Computation + var t; + for (var i = 0; i < 80; i += 1) { + t = (al + M[offset+zl[i]])|0; + if (i<16){ + t += f1(bl,cl,dl) + hl[0]; + } else if (i<32) { + t += f2(bl,cl,dl) + hl[1]; + } else if (i<48) { + t += f3(bl,cl,dl) + hl[2]; + } else if (i<64) { + t += f4(bl,cl,dl) + hl[3]; + } else {// if (i<80) { + t += f5(bl,cl,dl) + hl[4]; + } + t = t|0; + t = rotl(t,sl[i]); + t = (t+el)|0; + al = el; + el = dl; + dl = rotl(cl, 10); + cl = bl; + bl = t; + + t = (ar + M[offset+zr[i]])|0; + if (i<16){ + t += f5(br,cr,dr) + hr[0]; + } else if (i<32) { + t += f4(br,cr,dr) + hr[1]; + } else if (i<48) { + t += f3(br,cr,dr) + hr[2]; + } else if (i<64) { + t += f2(br,cr,dr) + hr[3]; + } else {// if (i<80) { + t += f1(br,cr,dr) + hr[4]; + } + t = t|0; + t = rotl(t,sr[i]) ; + t = (t+er)|0; + ar = er; + er = dr; + dr = rotl(cr, 10); + cr = br; + br = t; + } + // Intermediate hash value + t = (H[1] + cl + dr)|0; + H[1] = (H[2] + dl + er)|0; + H[2] = (H[3] + el + ar)|0; + H[3] = (H[4] + al + br)|0; + H[4] = (H[0] + bl + cr)|0; + H[0] = t; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotal << 8) | (nBitsTotal >>> 24)) & 0x00ff00ff) | + (((nBitsTotal << 24) | (nBitsTotal >>> 8)) & 0xff00ff00) + ); + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 5; i++) { + // Shortcut + var H_i = H[i]; + + // Swap + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + + function f1(x, y, z) { + return ((x) ^ (y) ^ (z)); + + } + + function f2(x, y, z) { + return (((x)&(y)) | ((~x)&(z))); + } + + function f3(x, y, z) { + return (((x) | (~(y))) ^ (z)); + } + + function f4(x, y, z) { + return (((x) & (z)) | ((y)&(~(z)))); + } + + function f5(x, y, z) { + return ((x) ^ ((y) |(~(z)))); + + } + + function rotl(x,n) { + return (x<>>(32-n)); + } + + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.RIPEMD160('message'); + * var hash = CryptoJS.RIPEMD160(wordArray); + */ + C.RIPEMD160 = Hasher._createHelper(RIPEMD160); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacRIPEMD160(message, key); + */ + C.HmacRIPEMD160 = Hasher._createHmacHelper(RIPEMD160); + }(Math)); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var C_algo = C.algo; + + /** + * HMAC algorithm. + */ + var HMAC = C_algo.HMAC = Base.extend({ + /** + * Initializes a newly created HMAC. + * + * @param {Hasher} hasher The hash algorithm to use. + * @param {WordArray|string} key The secret key. + * + * @example + * + * var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key); + */ + init: function (hasher, key) { + // Init hasher + hasher = this._hasher = new hasher.init(); + + // Convert string to WordArray, else assume WordArray already + if (typeof key == 'string') { + key = Utf8.parse(key); + } + + // Shortcuts + var hasherBlockSize = hasher.blockSize; + var hasherBlockSizeBytes = hasherBlockSize * 4; + + // Allow arbitrary length keys + if (key.sigBytes > hasherBlockSizeBytes) { + key = hasher.finalize(key); + } + + // Clamp excess bits + key.clamp(); + + // Clone key for inner and outer pads + var oKey = this._oKey = key.clone(); + var iKey = this._iKey = key.clone(); + + // Shortcuts + var oKeyWords = oKey.words; + var iKeyWords = iKey.words; + + // XOR keys with pad constants + for (var i = 0; i < hasherBlockSize; i++) { + oKeyWords[i] ^= 0x5c5c5c5c; + iKeyWords[i] ^= 0x36363636; + } + oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes; + + // Set initial values + this.reset(); + }, + + /** + * Resets this HMAC to its initial state. + * + * @example + * + * hmacHasher.reset(); + */ + reset: function () { + // Shortcut + var hasher = this._hasher; + + // Reset + hasher.reset(); + hasher.update(this._iKey); + }, + + /** + * Updates this HMAC with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {HMAC} This HMAC instance. + * + * @example + * + * hmacHasher.update('message'); + * hmacHasher.update(wordArray); + */ + update: function (messageUpdate) { + this._hasher.update(messageUpdate); + + // Chainable + return this; + }, + + /** + * Finalizes the HMAC computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The HMAC. + * + * @example + * + * var hmac = hmacHasher.finalize(); + * var hmac = hmacHasher.finalize('message'); + * var hmac = hmacHasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Shortcut + var hasher = this._hasher; + + // Compute HMAC + var innerHash = hasher.finalize(messageUpdate); + hasher.reset(); + var hmac = hasher.finalize(this._oKey.clone().concat(innerHash)); + + return hmac; + } + }); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var SHA1 = C_algo.SHA1; + var HMAC = C_algo.HMAC; + + /** + * Password-Based Key Derivation Function 2 algorithm. + */ + var PBKDF2 = C_algo.PBKDF2 = Base.extend({ + /** + * Configuration options. + * + * @property {number} keySize The key size in words to generate. Default: 4 (128 bits) + * @property {Hasher} hasher The hasher to use. Default: SHA1 + * @property {number} iterations The number of iterations to perform. Default: 1 + */ + cfg: Base.extend({ + keySize: 128/32, + hasher: SHA1, + iterations: 1 + }), + + /** + * Initializes a newly created key derivation function. + * + * @param {Object} cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.PBKDF2.create(); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 }); + */ + init: function (cfg) { + this.cfg = this.cfg.extend(cfg); + }, + + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * + * @return {WordArray} The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute: function (password, salt) { + // Shortcut + var cfg = this.cfg; + + // Init HMAC + var hmac = HMAC.create(cfg.hasher, password); + + // Initial values + var derivedKey = WordArray.create(); + var blockIndex = WordArray.create([0x00000001]); + + // Shortcuts + var derivedKeyWords = derivedKey.words; + var blockIndexWords = blockIndex.words; + var keySize = cfg.keySize; + var iterations = cfg.iterations; + + // Generate key + while (derivedKeyWords.length < keySize) { + var block = hmac.update(salt).finalize(blockIndex); + hmac.reset(); + + // Shortcuts + var blockWords = block.words; + var blockWordsLength = blockWords.length; + + // Iterations + var intermediate = block; + for (var i = 1; i < iterations; i++) { + intermediate = hmac.finalize(intermediate); + hmac.reset(); + + // Shortcut + var intermediateWords = intermediate.words; + + // XOR intermediate with block + for (var j = 0; j < blockWordsLength; j++) { + blockWords[j] ^= intermediateWords[j]; + } + } + + derivedKey.concat(block); + blockIndexWords[0]++; + } + derivedKey.sigBytes = keySize * 4; + + return derivedKey; + } + }); + + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * @param {Object} cfg (Optional) The configuration options to use for this computation. + * + * @return {WordArray} The derived key. + * + * @static + * + * @example + * + * var key = CryptoJS.PBKDF2(password, salt); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 }); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 }); + */ + C.PBKDF2 = function (password, salt, cfg) { + return PBKDF2.create(cfg).compute(password, salt); + }; + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var MD5 = C_algo.MD5; + + /** + * This key derivation function is meant to conform with EVP_BytesToKey. + * www.openssl.org/docs/crypto/EVP_BytesToKey.html + */ + var EvpKDF = C_algo.EvpKDF = Base.extend({ + /** + * Configuration options. + * + * @property {number} keySize The key size in words to generate. Default: 4 (128 bits) + * @property {Hasher} hasher The hash algorithm to use. Default: MD5 + * @property {number} iterations The number of iterations to perform. Default: 1 + */ + cfg: Base.extend({ + keySize: 128/32, + hasher: MD5, + iterations: 1 + }), + + /** + * Initializes a newly created key derivation function. + * + * @param {Object} cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.EvpKDF.create(); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8, iterations: 1000 }); + */ + init: function (cfg) { + this.cfg = this.cfg.extend(cfg); + }, + + /** + * Derives a key from a password. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * + * @return {WordArray} The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute: function (password, salt) { + var block; + + // Shortcut + var cfg = this.cfg; + + // Init hasher + var hasher = cfg.hasher.create(); + + // Initial values + var derivedKey = WordArray.create(); + + // Shortcuts + var derivedKeyWords = derivedKey.words; + var keySize = cfg.keySize; + var iterations = cfg.iterations; + + // Generate key + while (derivedKeyWords.length < keySize) { + if (block) { + hasher.update(block); + } + block = hasher.update(password).finalize(salt); + hasher.reset(); + + // Iterations + for (var i = 1; i < iterations; i++) { + block = hasher.finalize(block); + hasher.reset(); + } + + derivedKey.concat(block); + } + derivedKey.sigBytes = keySize * 4; + + return derivedKey; + } + }); + + /** + * Derives a key from a password. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * @param {Object} cfg (Optional) The configuration options to use for this computation. + * + * @return {WordArray} The derived key. + * + * @static + * + * @example + * + * var key = CryptoJS.EvpKDF(password, salt); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8 }); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8, iterations: 1000 }); + */ + C.EvpKDF = function (password, salt, cfg) { + return EvpKDF.create(cfg).compute(password, salt); + }; + }()); + + + /** + * Cipher core components. + */ + CryptoJS.lib.Cipher || (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var Base64 = C_enc.Base64; + var C_algo = C.algo; + var EvpKDF = C_algo.EvpKDF; + + /** + * Abstract base cipher template. + * + * @property {number} keySize This cipher's key size. Default: 4 (128 bits) + * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits) + * @property {number} _ENC_XFORM_MODE A constant representing encryption mode. + * @property {number} _DEC_XFORM_MODE A constant representing decryption mode. + */ + var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + * + * @property {WordArray} iv The IV to use for this operation. + */ + cfg: Base.extend(), + + /** + * Creates this cipher in encryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray }); + */ + createEncryptor: function (key, cfg) { + return this.create(this._ENC_XFORM_MODE, key, cfg); + }, + + /** + * Creates this cipher in decryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray }); + */ + createDecryptor: function (key, cfg) { + return this.create(this._DEC_XFORM_MODE, key, cfg); + }, + + /** + * Initializes a newly created cipher. + * + * @param {number} xformMode Either the encryption or decryption transormation mode constant. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @example + * + * var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray }); + */ + init: function (xformMode, key, cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Store transform mode and key + this._xformMode = xformMode; + this._key = key; + + // Set initial values + this.reset(); + }, + + /** + * Resets this cipher to its initial state. + * + * @example + * + * cipher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-cipher logic + this._doReset(); + }, + + /** + * Adds data to be encrypted or decrypted. + * + * @param {WordArray|string} dataUpdate The data to encrypt or decrypt. + * + * @return {WordArray} The data after processing. + * + * @example + * + * var encrypted = cipher.process('data'); + * var encrypted = cipher.process(wordArray); + */ + process: function (dataUpdate) { + // Append + this._append(dataUpdate); + + // Process available blocks + return this._process(); + }, + + /** + * Finalizes the encryption or decryption process. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt. + * + * @return {WordArray} The data after final processing. + * + * @example + * + * var encrypted = cipher.finalize(); + * var encrypted = cipher.finalize('data'); + * var encrypted = cipher.finalize(wordArray); + */ + finalize: function (dataUpdate) { + // Final data update + if (dataUpdate) { + this._append(dataUpdate); + } + + // Perform concrete-cipher logic + var finalProcessedData = this._doFinalize(); + + return finalProcessedData; + }, + + keySize: 128/32, + + ivSize: 128/32, + + _ENC_XFORM_MODE: 1, + + _DEC_XFORM_MODE: 2, + + /** + * Creates shortcut functions to a cipher's object interface. + * + * @param {Cipher} cipher The cipher to create a helper for. + * + * @return {Object} An object with encrypt and decrypt shortcut functions. + * + * @static + * + * @example + * + * var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES); + */ + _createHelper: (function () { + function selectCipherStrategy(key) { + if (typeof key == 'string') { + return PasswordBasedCipher; + } else { + return SerializableCipher; + } + } + + return function (cipher) { + return { + encrypt: function (message, key, cfg) { + return selectCipherStrategy(key).encrypt(cipher, message, key, cfg); + }, + + decrypt: function (ciphertext, key, cfg) { + return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg); + } + }; + }; + }()) + }); + + /** + * Abstract base stream cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits) + */ + var StreamCipher = C_lib.StreamCipher = Cipher.extend({ + _doFinalize: function () { + // Process partial blocks + var finalProcessedBlocks = this._process(!!'flush'); + + return finalProcessedBlocks; + }, + + blockSize: 1 + }); + + /** + * Mode namespace. + */ + var C_mode = C.mode = {}; + + /** + * Abstract base block cipher mode template. + */ + var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({ + /** + * Creates this mode for encryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words); + */ + createEncryptor: function (cipher, iv) { + return this.Encryptor.create(cipher, iv); + }, + + /** + * Creates this mode for decryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words); + */ + createDecryptor: function (cipher, iv) { + return this.Decryptor.create(cipher, iv); + }, + + /** + * Initializes a newly created mode. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words); + */ + init: function (cipher, iv) { + this._cipher = cipher; + this._iv = iv; + } + }); + + /** + * Cipher Block Chaining mode. + */ + var CBC = C_mode.CBC = (function () { + /** + * Abstract base CBC mode. + */ + var CBC = BlockCipherMode.extend(); + + /** + * CBC encryptor. + */ + CBC.Encryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // XOR and encrypt + xorBlock.call(this, words, offset, blockSize); + cipher.encryptBlock(words, offset); + + // Remember this block to use with next block + this._prevBlock = words.slice(offset, offset + blockSize); + } + }); + + /** + * CBC decryptor. + */ + CBC.Decryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // Remember this block to use with next block + var thisBlock = words.slice(offset, offset + blockSize); + + // Decrypt and XOR + cipher.decryptBlock(words, offset); + xorBlock.call(this, words, offset, blockSize); + + // This block becomes the previous block + this._prevBlock = thisBlock; + } + }); + + function xorBlock(words, offset, blockSize) { + var block; + + // Shortcut + var iv = this._iv; + + // Choose mixing block + if (iv) { + block = iv; + + // Remove IV for subsequent blocks + this._iv = undefined; + } else { + block = this._prevBlock; + } + + // XOR blocks + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= block[i]; + } + } + + return CBC; + }()); + + /** + * Padding namespace. + */ + var C_pad = C.pad = {}; + + /** + * PKCS #5/7 padding strategy. + */ + var Pkcs7 = C_pad.Pkcs7 = { + /** + * Pads data using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to pad. + * @param {number} blockSize The multiple that the data should be padded to. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.pad(wordArray, 4); + */ + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Create padding word + var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes; + + // Create padding + var paddingWords = []; + for (var i = 0; i < nPaddingBytes; i += 4) { + paddingWords.push(paddingWord); + } + var padding = WordArray.create(paddingWords, nPaddingBytes); + + // Add padding + data.concat(padding); + }, + + /** + * Unpads data that had been padded using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to unpad. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.unpad(wordArray); + */ + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + /** + * Abstract base block cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits) + */ + var BlockCipher = C_lib.BlockCipher = Cipher.extend({ + /** + * Configuration options. + * + * @property {Mode} mode The block mode to use. Default: CBC + * @property {Padding} padding The padding strategy to use. Default: Pkcs7 + */ + cfg: Cipher.cfg.extend({ + mode: CBC, + padding: Pkcs7 + }), + + reset: function () { + var modeCreator; + + // Reset cipher + Cipher.reset.call(this); + + // Shortcuts + var cfg = this.cfg; + var iv = cfg.iv; + var mode = cfg.mode; + + // Reset block mode + if (this._xformMode == this._ENC_XFORM_MODE) { + modeCreator = mode.createEncryptor; + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + modeCreator = mode.createDecryptor; + // Keep at least one block in the buffer for unpadding + this._minBufferSize = 1; + } + + if (this._mode && this._mode.__creator == modeCreator) { + this._mode.init(this, iv && iv.words); + } else { + this._mode = modeCreator.call(mode, this, iv && iv.words); + this._mode.__creator = modeCreator; + } + }, + + _doProcessBlock: function (words, offset) { + this._mode.processBlock(words, offset); + }, + + _doFinalize: function () { + var finalProcessedBlocks; + + // Shortcut + var padding = this.cfg.padding; + + // Finalize + if (this._xformMode == this._ENC_XFORM_MODE) { + // Pad data + padding.pad(this._data, this.blockSize); + + // Process final blocks + finalProcessedBlocks = this._process(!!'flush'); + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + // Process final blocks + finalProcessedBlocks = this._process(!!'flush'); + + // Unpad data + padding.unpad(finalProcessedBlocks); + } + + return finalProcessedBlocks; + }, + + blockSize: 128/32 + }); + + /** + * A collection of cipher parameters. + * + * @property {WordArray} ciphertext The raw ciphertext. + * @property {WordArray} key The key to this ciphertext. + * @property {WordArray} iv The IV used in the ciphering operation. + * @property {WordArray} salt The salt used with a key derivation function. + * @property {Cipher} algorithm The cipher algorithm. + * @property {Mode} mode The block mode used in the ciphering operation. + * @property {Padding} padding The padding scheme used in the ciphering operation. + * @property {number} blockSize The block size of the cipher. + * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string. + */ + var CipherParams = C_lib.CipherParams = Base.extend({ + /** + * Initializes a newly created cipher params object. + * + * @param {Object} cipherParams An object with any of the possible cipher parameters. + * + * @example + * + * var cipherParams = CryptoJS.lib.CipherParams.create({ + * ciphertext: ciphertextWordArray, + * key: keyWordArray, + * iv: ivWordArray, + * salt: saltWordArray, + * algorithm: CryptoJS.algo.AES, + * mode: CryptoJS.mode.CBC, + * padding: CryptoJS.pad.PKCS7, + * blockSize: 4, + * formatter: CryptoJS.format.OpenSSL + * }); + */ + init: function (cipherParams) { + this.mixIn(cipherParams); + }, + + /** + * Converts this cipher params object to a string. + * + * @param {Format} formatter (Optional) The formatting strategy to use. + * + * @return {string} The stringified cipher params. + * + * @throws Error If neither the formatter nor the default formatter is set. + * + * @example + * + * var string = cipherParams + ''; + * var string = cipherParams.toString(); + * var string = cipherParams.toString(CryptoJS.format.OpenSSL); + */ + toString: function (formatter) { + return (formatter || this.formatter).stringify(this); + } + }); + + /** + * Format namespace. + */ + var C_format = C.format = {}; + + /** + * OpenSSL formatting strategy. + */ + var OpenSSLFormatter = C_format.OpenSSL = { + /** + * Converts a cipher params object to an OpenSSL-compatible string. + * + * @param {CipherParams} cipherParams The cipher params object. + * + * @return {string} The OpenSSL-compatible string. + * + * @static + * + * @example + * + * var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams); + */ + stringify: function (cipherParams) { + var wordArray; + + // Shortcuts + var ciphertext = cipherParams.ciphertext; + var salt = cipherParams.salt; + + // Format + if (salt) { + wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext); + } else { + wordArray = ciphertext; + } + + return wordArray.toString(Base64); + }, + + /** + * Converts an OpenSSL-compatible string to a cipher params object. + * + * @param {string} openSSLStr The OpenSSL-compatible string. + * + * @return {CipherParams} The cipher params object. + * + * @static + * + * @example + * + * var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString); + */ + parse: function (openSSLStr) { + var salt; + + // Parse base64 + var ciphertext = Base64.parse(openSSLStr); + + // Shortcut + var ciphertextWords = ciphertext.words; + + // Test for salt + if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) { + // Extract salt + salt = WordArray.create(ciphertextWords.slice(2, 4)); + + // Remove salt from ciphertext + ciphertextWords.splice(0, 4); + ciphertext.sigBytes -= 16; + } + + return CipherParams.create({ ciphertext: ciphertext, salt: salt }); + } + }; + + /** + * A cipher wrapper that returns ciphertext as a serializable cipher params object. + */ + var SerializableCipher = C_lib.SerializableCipher = Base.extend({ + /** + * Configuration options. + * + * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL + */ + cfg: Base.extend({ + format: OpenSSLFormatter + }), + + /** + * Encrypts a message. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv }); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Encrypt + var encryptor = cipher.createEncryptor(key, cfg); + var ciphertext = encryptor.finalize(message); + + // Shortcut + var cipherCfg = encryptor.cfg; + + // Create and return serializable cipher params + return CipherParams.create({ + ciphertext: ciphertext, + key: key, + iv: cipherCfg.iv, + algorithm: cipher, + mode: cipherCfg.mode, + padding: cipherCfg.padding, + blockSize: cipher.blockSize, + formatter: cfg.format + }); + }, + + /** + * Decrypts serialized ciphertext. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Decrypt + var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext); + + return plaintext; + }, + + /** + * Converts serialized ciphertext to CipherParams, + * else assumed CipherParams already and returns ciphertext unchanged. + * + * @param {CipherParams|string} ciphertext The ciphertext. + * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext. + * + * @return {CipherParams} The unserialized ciphertext. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format); + */ + _parse: function (ciphertext, format) { + if (typeof ciphertext == 'string') { + return format.parse(ciphertext, this); + } else { + return ciphertext; + } + } + }); + + /** + * Key derivation function namespace. + */ + var C_kdf = C.kdf = {}; + + /** + * OpenSSL key derivation function. + */ + var OpenSSLKdf = C_kdf.OpenSSL = { + /** + * Derives a key and IV from a password. + * + * @param {string} password The password to derive from. + * @param {number} keySize The size in words of the key to generate. + * @param {number} ivSize The size in words of the IV to generate. + * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly. + * + * @return {CipherParams} A cipher params object with the key, IV, and salt. + * + * @static + * + * @example + * + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32); + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt'); + */ + execute: function (password, keySize, ivSize, salt) { + // Generate random salt + if (!salt) { + salt = WordArray.random(64/8); + } + + // Derive key and IV + var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt); + + // Separate key and IV + var iv = WordArray.create(key.words.slice(keySize), ivSize * 4); + key.sigBytes = keySize * 4; + + // Return params + return CipherParams.create({ key: key, iv: iv, salt: salt }); + } + }; + + /** + * A serializable cipher wrapper that derives the key from a password, + * and returns ciphertext as a serializable cipher params object. + */ + var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({ + /** + * Configuration options. + * + * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL + */ + cfg: SerializableCipher.cfg.extend({ + kdf: OpenSSLKdf + }), + + /** + * Encrypts a message using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password'); + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Encrypt + var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg); + + // Mix in derived params + ciphertext.mixIn(derivedParams); + + return ciphertext; + }, + + /** + * Decrypts serialized ciphertext using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Decrypt + var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg); + + return plaintext; + } + }); + }()); + + + /** + * Cipher Feedback block mode. + */ + CryptoJS.mode.CFB = (function () { + var CFB = CryptoJS.lib.BlockCipherMode.extend(); + + CFB.Encryptor = CFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher); + + // Remember this block to use with next block + this._prevBlock = words.slice(offset, offset + blockSize); + } + }); + + CFB.Decryptor = CFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // Remember this block to use with next block + var thisBlock = words.slice(offset, offset + blockSize); + + generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher); + + // This block becomes the previous block + this._prevBlock = thisBlock; + } + }); + + function generateKeystreamAndEncrypt(words, offset, blockSize, cipher) { + var keystream; + + // Shortcut + var iv = this._iv; + + // Generate keystream + if (iv) { + keystream = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } else { + keystream = this._prevBlock; + } + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + + return CFB; + }()); + + + /** + * Counter block mode. + */ + CryptoJS.mode.CTR = (function () { + var CTR = CryptoJS.lib.BlockCipherMode.extend(); + + var Encryptor = CTR.Encryptor = CTR.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var counter = this._counter; + + // Generate keystream + if (iv) { + counter = this._counter = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + var keystream = counter.slice(0); + cipher.encryptBlock(keystream, 0); + + // Increment counter + counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0 + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + CTR.Decryptor = Encryptor; + + return CTR; + }()); + + + /** @preserve + * Counter block mode compatible with Dr Brian Gladman fileenc.c + * derived from CryptoJS.mode.CTR + * Jan Hruby jhruby.web@gmail.com + */ + CryptoJS.mode.CTRGladman = (function () { + var CTRGladman = CryptoJS.lib.BlockCipherMode.extend(); + + function incWord(word) + { + if (((word >> 24) & 0xff) === 0xff) { //overflow + var b1 = (word >> 16)&0xff; + var b2 = (word >> 8)&0xff; + var b3 = word & 0xff; + + if (b1 === 0xff) // overflow b1 + { + b1 = 0; + if (b2 === 0xff) + { + b2 = 0; + if (b3 === 0xff) + { + b3 = 0; + } + else + { + ++b3; + } + } + else + { + ++b2; + } + } + else + { + ++b1; + } + + word = 0; + word += (b1 << 16); + word += (b2 << 8); + word += b3; + } + else + { + word += (0x01 << 24); + } + return word; + } + + function incCounter(counter) + { + if ((counter[0] = incWord(counter[0])) === 0) + { + // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8 + counter[1] = incWord(counter[1]); + } + return counter; + } + + var Encryptor = CTRGladman.Encryptor = CTRGladman.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var counter = this._counter; + + // Generate keystream + if (iv) { + counter = this._counter = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + + incCounter(counter); + + var keystream = counter.slice(0); + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + CTRGladman.Decryptor = Encryptor; + + return CTRGladman; + }()); + + + + + /** + * Output Feedback block mode. + */ + CryptoJS.mode.OFB = (function () { + var OFB = CryptoJS.lib.BlockCipherMode.extend(); + + var Encryptor = OFB.Encryptor = OFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var keystream = this._keystream; + + // Generate keystream + if (iv) { + keystream = this._keystream = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + OFB.Decryptor = Encryptor; + + return OFB; + }()); + + + /** + * Electronic Codebook block mode. + */ + CryptoJS.mode.ECB = (function () { + var ECB = CryptoJS.lib.BlockCipherMode.extend(); + + ECB.Encryptor = ECB.extend({ + processBlock: function (words, offset) { + this._cipher.encryptBlock(words, offset); + } + }); + + ECB.Decryptor = ECB.extend({ + processBlock: function (words, offset) { + this._cipher.decryptBlock(words, offset); + } + }); + + return ECB; + }()); + + + /** + * ANSI X.923 padding strategy. + */ + CryptoJS.pad.AnsiX923 = { + pad: function (data, blockSize) { + // Shortcuts + var dataSigBytes = data.sigBytes; + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - dataSigBytes % blockSizeBytes; + + // Compute last byte position + var lastBytePos = dataSigBytes + nPaddingBytes - 1; + + // Pad + data.clamp(); + data.words[lastBytePos >>> 2] |= nPaddingBytes << (24 - (lastBytePos % 4) * 8); + data.sigBytes += nPaddingBytes; + }, + + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + + /** + * ISO 10126 padding strategy. + */ + CryptoJS.pad.Iso10126 = { + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Pad + data.concat(CryptoJS.lib.WordArray.random(nPaddingBytes - 1)). + concat(CryptoJS.lib.WordArray.create([nPaddingBytes << 24], 1)); + }, + + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + + /** + * ISO/IEC 9797-1 Padding Method 2. + */ + CryptoJS.pad.Iso97971 = { + pad: function (data, blockSize) { + // Add 0x80 byte + data.concat(CryptoJS.lib.WordArray.create([0x80000000], 1)); + + // Zero pad the rest + CryptoJS.pad.ZeroPadding.pad(data, blockSize); + }, + + unpad: function (data) { + // Remove zero padding + CryptoJS.pad.ZeroPadding.unpad(data); + + // Remove one more byte -- the 0x80 byte + data.sigBytes--; + } + }; + + + /** + * Zero padding strategy. + */ + CryptoJS.pad.ZeroPadding = { + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Pad + data.clamp(); + data.sigBytes += blockSizeBytes - ((data.sigBytes % blockSizeBytes) || blockSizeBytes); + }, + + unpad: function (data) { + // Shortcut + var dataWords = data.words; + + // Unpad + var i = data.sigBytes - 1; + for (var i = data.sigBytes - 1; i >= 0; i--) { + if (((dataWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff)) { + data.sigBytes = i + 1; + break; + } + } + } + }; + + + /** + * A noop padding strategy. + */ + CryptoJS.pad.NoPadding = { + pad: function () { + }, + + unpad: function () { + } + }; + + + (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var CipherParams = C_lib.CipherParams; + var C_enc = C.enc; + var Hex = C_enc.Hex; + var C_format = C.format; + + var HexFormatter = C_format.Hex = { + /** + * Converts the ciphertext of a cipher params object to a hexadecimally encoded string. + * + * @param {CipherParams} cipherParams The cipher params object. + * + * @return {string} The hexadecimally encoded string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.format.Hex.stringify(cipherParams); + */ + stringify: function (cipherParams) { + return cipherParams.ciphertext.toString(Hex); + }, + + /** + * Converts a hexadecimally encoded ciphertext string to a cipher params object. + * + * @param {string} input The hexadecimally encoded string. + * + * @return {CipherParams} The cipher params object. + * + * @static + * + * @example + * + * var cipherParams = CryptoJS.format.Hex.parse(hexString); + */ + parse: function (input) { + var ciphertext = Hex.parse(input); + return CipherParams.create({ ciphertext: ciphertext }); + } + }; + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var BlockCipher = C_lib.BlockCipher; + var C_algo = C.algo; + + // Lookup tables + var SBOX = []; + var INV_SBOX = []; + var SUB_MIX_0 = []; + var SUB_MIX_1 = []; + var SUB_MIX_2 = []; + var SUB_MIX_3 = []; + var INV_SUB_MIX_0 = []; + var INV_SUB_MIX_1 = []; + var INV_SUB_MIX_2 = []; + var INV_SUB_MIX_3 = []; + + // Compute lookup tables + (function () { + // Compute double table + var d = []; + for (var i = 0; i < 256; i++) { + if (i < 128) { + d[i] = i << 1; + } else { + d[i] = (i << 1) ^ 0x11b; + } + } + + // Walk GF(2^8) + var x = 0; + var xi = 0; + for (var i = 0; i < 256; i++) { + // Compute sbox + var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4); + sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63; + SBOX[x] = sx; + INV_SBOX[sx] = x; + + // Compute multiplication + var x2 = d[x]; + var x4 = d[x2]; + var x8 = d[x4]; + + // Compute sub bytes, mix columns tables + var t = (d[sx] * 0x101) ^ (sx * 0x1010100); + SUB_MIX_0[x] = (t << 24) | (t >>> 8); + SUB_MIX_1[x] = (t << 16) | (t >>> 16); + SUB_MIX_2[x] = (t << 8) | (t >>> 24); + SUB_MIX_3[x] = t; + + // Compute inv sub bytes, inv mix columns tables + var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100); + INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8); + INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16); + INV_SUB_MIX_2[sx] = (t << 8) | (t >>> 24); + INV_SUB_MIX_3[sx] = t; + + // Compute next counter + if (!x) { + x = xi = 1; + } else { + x = x2 ^ d[d[d[x8 ^ x2]]]; + xi ^= d[d[xi]]; + } + } + }()); + + // Precomputed Rcon lookup + var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + + /** + * AES block cipher algorithm. + */ + var AES = C_algo.AES = BlockCipher.extend({ + _doReset: function () { + var t; + + // Skip reset of nRounds has been set before and key did not change + if (this._nRounds && this._keyPriorReset === this._key) { + return; + } + + // Shortcuts + var key = this._keyPriorReset = this._key; + var keyWords = key.words; + var keySize = key.sigBytes / 4; + + // Compute number of rounds + var nRounds = this._nRounds = keySize + 6; + + // Compute number of key schedule rows + var ksRows = (nRounds + 1) * 4; + + // Compute key schedule + var keySchedule = this._keySchedule = []; + for (var ksRow = 0; ksRow < ksRows; ksRow++) { + if (ksRow < keySize) { + keySchedule[ksRow] = keyWords[ksRow]; + } else { + t = keySchedule[ksRow - 1]; + + if (!(ksRow % keySize)) { + // Rot word + t = (t << 8) | (t >>> 24); + + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + + // Mix Rcon + t ^= RCON[(ksRow / keySize) | 0] << 24; + } else if (keySize > 6 && ksRow % keySize == 4) { + // Sub word + t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; + } + + keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t; + } + } + + // Compute inv key schedule + var invKeySchedule = this._invKeySchedule = []; + for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) { + var ksRow = ksRows - invKsRow; + + if (invKsRow % 4) { + var t = keySchedule[ksRow]; + } else { + var t = keySchedule[ksRow - 4]; + } + + if (invKsRow < 4 || ksRow <= 4) { + invKeySchedule[invKsRow] = t; + } else { + invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^ + INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]]; + } + } + }, + + encryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX); + }, + + decryptBlock: function (M, offset) { + // Swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + + this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX); + + // Inv swap 2nd and 4th rows + var t = M[offset + 1]; + M[offset + 1] = M[offset + 3]; + M[offset + 3] = t; + }, + + _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) { + // Shortcut + var nRounds = this._nRounds; + + // Get input, add round key + var s0 = M[offset] ^ keySchedule[0]; + var s1 = M[offset + 1] ^ keySchedule[1]; + var s2 = M[offset + 2] ^ keySchedule[2]; + var s3 = M[offset + 3] ^ keySchedule[3]; + + // Key schedule row counter + var ksRow = 4; + + // Rounds + for (var round = 1; round < nRounds; round++) { + // Shift rows, sub bytes, mix columns, add round key + var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++]; + var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++]; + var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++]; + var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++]; + + // Update state + s0 = t0; + s1 = t1; + s2 = t2; + s3 = t3; + } + + // Shift rows, sub bytes, add round key + var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++]; + var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++]; + var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++]; + var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++]; + + // Set output + M[offset] = t0; + M[offset + 1] = t1; + M[offset + 2] = t2; + M[offset + 3] = t3; + }, + + keySize: 256/32 + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.AES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.AES.decrypt(ciphertext, key, cfg); + */ + C.AES = BlockCipher._createHelper(AES); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var BlockCipher = C_lib.BlockCipher; + var C_algo = C.algo; + + // Permuted Choice 1 constants + var PC1 = [ + 57, 49, 41, 33, 25, 17, 9, 1, + 58, 50, 42, 34, 26, 18, 10, 2, + 59, 51, 43, 35, 27, 19, 11, 3, + 60, 52, 44, 36, 63, 55, 47, 39, + 31, 23, 15, 7, 62, 54, 46, 38, + 30, 22, 14, 6, 61, 53, 45, 37, + 29, 21, 13, 5, 28, 20, 12, 4 + ]; + + // Permuted Choice 2 constants + var PC2 = [ + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32 + ]; + + // Cumulative bit shift constants + var BIT_SHIFTS = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28]; + + // SBOXes and round permutation constants + var SBOX_P = [ + { + 0x0: 0x808200, + 0x10000000: 0x8000, + 0x20000000: 0x808002, + 0x30000000: 0x2, + 0x40000000: 0x200, + 0x50000000: 0x808202, + 0x60000000: 0x800202, + 0x70000000: 0x800000, + 0x80000000: 0x202, + 0x90000000: 0x800200, + 0xa0000000: 0x8200, + 0xb0000000: 0x808000, + 0xc0000000: 0x8002, + 0xd0000000: 0x800002, + 0xe0000000: 0x0, + 0xf0000000: 0x8202, + 0x8000000: 0x0, + 0x18000000: 0x808202, + 0x28000000: 0x8202, + 0x38000000: 0x8000, + 0x48000000: 0x808200, + 0x58000000: 0x200, + 0x68000000: 0x808002, + 0x78000000: 0x2, + 0x88000000: 0x800200, + 0x98000000: 0x8200, + 0xa8000000: 0x808000, + 0xb8000000: 0x800202, + 0xc8000000: 0x800002, + 0xd8000000: 0x8002, + 0xe8000000: 0x202, + 0xf8000000: 0x800000, + 0x1: 0x8000, + 0x10000001: 0x2, + 0x20000001: 0x808200, + 0x30000001: 0x800000, + 0x40000001: 0x808002, + 0x50000001: 0x8200, + 0x60000001: 0x200, + 0x70000001: 0x800202, + 0x80000001: 0x808202, + 0x90000001: 0x808000, + 0xa0000001: 0x800002, + 0xb0000001: 0x8202, + 0xc0000001: 0x202, + 0xd0000001: 0x800200, + 0xe0000001: 0x8002, + 0xf0000001: 0x0, + 0x8000001: 0x808202, + 0x18000001: 0x808000, + 0x28000001: 0x800000, + 0x38000001: 0x200, + 0x48000001: 0x8000, + 0x58000001: 0x800002, + 0x68000001: 0x2, + 0x78000001: 0x8202, + 0x88000001: 0x8002, + 0x98000001: 0x800202, + 0xa8000001: 0x202, + 0xb8000001: 0x808200, + 0xc8000001: 0x800200, + 0xd8000001: 0x0, + 0xe8000001: 0x8200, + 0xf8000001: 0x808002 + }, + { + 0x0: 0x40084010, + 0x1000000: 0x4000, + 0x2000000: 0x80000, + 0x3000000: 0x40080010, + 0x4000000: 0x40000010, + 0x5000000: 0x40084000, + 0x6000000: 0x40004000, + 0x7000000: 0x10, + 0x8000000: 0x84000, + 0x9000000: 0x40004010, + 0xa000000: 0x40000000, + 0xb000000: 0x84010, + 0xc000000: 0x80010, + 0xd000000: 0x0, + 0xe000000: 0x4010, + 0xf000000: 0x40080000, + 0x800000: 0x40004000, + 0x1800000: 0x84010, + 0x2800000: 0x10, + 0x3800000: 0x40004010, + 0x4800000: 0x40084010, + 0x5800000: 0x40000000, + 0x6800000: 0x80000, + 0x7800000: 0x40080010, + 0x8800000: 0x80010, + 0x9800000: 0x0, + 0xa800000: 0x4000, + 0xb800000: 0x40080000, + 0xc800000: 0x40000010, + 0xd800000: 0x84000, + 0xe800000: 0x40084000, + 0xf800000: 0x4010, + 0x10000000: 0x0, + 0x11000000: 0x40080010, + 0x12000000: 0x40004010, + 0x13000000: 0x40084000, + 0x14000000: 0x40080000, + 0x15000000: 0x10, + 0x16000000: 0x84010, + 0x17000000: 0x4000, + 0x18000000: 0x4010, + 0x19000000: 0x80000, + 0x1a000000: 0x80010, + 0x1b000000: 0x40000010, + 0x1c000000: 0x84000, + 0x1d000000: 0x40004000, + 0x1e000000: 0x40000000, + 0x1f000000: 0x40084010, + 0x10800000: 0x84010, + 0x11800000: 0x80000, + 0x12800000: 0x40080000, + 0x13800000: 0x4000, + 0x14800000: 0x40004000, + 0x15800000: 0x40084010, + 0x16800000: 0x10, + 0x17800000: 0x40000000, + 0x18800000: 0x40084000, + 0x19800000: 0x40000010, + 0x1a800000: 0x40004010, + 0x1b800000: 0x80010, + 0x1c800000: 0x0, + 0x1d800000: 0x4010, + 0x1e800000: 0x40080010, + 0x1f800000: 0x84000 + }, + { + 0x0: 0x104, + 0x100000: 0x0, + 0x200000: 0x4000100, + 0x300000: 0x10104, + 0x400000: 0x10004, + 0x500000: 0x4000004, + 0x600000: 0x4010104, + 0x700000: 0x4010000, + 0x800000: 0x4000000, + 0x900000: 0x4010100, + 0xa00000: 0x10100, + 0xb00000: 0x4010004, + 0xc00000: 0x4000104, + 0xd00000: 0x10000, + 0xe00000: 0x4, + 0xf00000: 0x100, + 0x80000: 0x4010100, + 0x180000: 0x4010004, + 0x280000: 0x0, + 0x380000: 0x4000100, + 0x480000: 0x4000004, + 0x580000: 0x10000, + 0x680000: 0x10004, + 0x780000: 0x104, + 0x880000: 0x4, + 0x980000: 0x100, + 0xa80000: 0x4010000, + 0xb80000: 0x10104, + 0xc80000: 0x10100, + 0xd80000: 0x4000104, + 0xe80000: 0x4010104, + 0xf80000: 0x4000000, + 0x1000000: 0x4010100, + 0x1100000: 0x10004, + 0x1200000: 0x10000, + 0x1300000: 0x4000100, + 0x1400000: 0x100, + 0x1500000: 0x4010104, + 0x1600000: 0x4000004, + 0x1700000: 0x0, + 0x1800000: 0x4000104, + 0x1900000: 0x4000000, + 0x1a00000: 0x4, + 0x1b00000: 0x10100, + 0x1c00000: 0x4010000, + 0x1d00000: 0x104, + 0x1e00000: 0x10104, + 0x1f00000: 0x4010004, + 0x1080000: 0x4000000, + 0x1180000: 0x104, + 0x1280000: 0x4010100, + 0x1380000: 0x0, + 0x1480000: 0x10004, + 0x1580000: 0x4000100, + 0x1680000: 0x100, + 0x1780000: 0x4010004, + 0x1880000: 0x10000, + 0x1980000: 0x4010104, + 0x1a80000: 0x10104, + 0x1b80000: 0x4000004, + 0x1c80000: 0x4000104, + 0x1d80000: 0x4010000, + 0x1e80000: 0x4, + 0x1f80000: 0x10100 + }, + { + 0x0: 0x80401000, + 0x10000: 0x80001040, + 0x20000: 0x401040, + 0x30000: 0x80400000, + 0x40000: 0x0, + 0x50000: 0x401000, + 0x60000: 0x80000040, + 0x70000: 0x400040, + 0x80000: 0x80000000, + 0x90000: 0x400000, + 0xa0000: 0x40, + 0xb0000: 0x80001000, + 0xc0000: 0x80400040, + 0xd0000: 0x1040, + 0xe0000: 0x1000, + 0xf0000: 0x80401040, + 0x8000: 0x80001040, + 0x18000: 0x40, + 0x28000: 0x80400040, + 0x38000: 0x80001000, + 0x48000: 0x401000, + 0x58000: 0x80401040, + 0x68000: 0x0, + 0x78000: 0x80400000, + 0x88000: 0x1000, + 0x98000: 0x80401000, + 0xa8000: 0x400000, + 0xb8000: 0x1040, + 0xc8000: 0x80000000, + 0xd8000: 0x400040, + 0xe8000: 0x401040, + 0xf8000: 0x80000040, + 0x100000: 0x400040, + 0x110000: 0x401000, + 0x120000: 0x80000040, + 0x130000: 0x0, + 0x140000: 0x1040, + 0x150000: 0x80400040, + 0x160000: 0x80401000, + 0x170000: 0x80001040, + 0x180000: 0x80401040, + 0x190000: 0x80000000, + 0x1a0000: 0x80400000, + 0x1b0000: 0x401040, + 0x1c0000: 0x80001000, + 0x1d0000: 0x400000, + 0x1e0000: 0x40, + 0x1f0000: 0x1000, + 0x108000: 0x80400000, + 0x118000: 0x80401040, + 0x128000: 0x0, + 0x138000: 0x401000, + 0x148000: 0x400040, + 0x158000: 0x80000000, + 0x168000: 0x80001040, + 0x178000: 0x40, + 0x188000: 0x80000040, + 0x198000: 0x1000, + 0x1a8000: 0x80001000, + 0x1b8000: 0x80400040, + 0x1c8000: 0x1040, + 0x1d8000: 0x80401000, + 0x1e8000: 0x400000, + 0x1f8000: 0x401040 + }, + { + 0x0: 0x80, + 0x1000: 0x1040000, + 0x2000: 0x40000, + 0x3000: 0x20000000, + 0x4000: 0x20040080, + 0x5000: 0x1000080, + 0x6000: 0x21000080, + 0x7000: 0x40080, + 0x8000: 0x1000000, + 0x9000: 0x20040000, + 0xa000: 0x20000080, + 0xb000: 0x21040080, + 0xc000: 0x21040000, + 0xd000: 0x0, + 0xe000: 0x1040080, + 0xf000: 0x21000000, + 0x800: 0x1040080, + 0x1800: 0x21000080, + 0x2800: 0x80, + 0x3800: 0x1040000, + 0x4800: 0x40000, + 0x5800: 0x20040080, + 0x6800: 0x21040000, + 0x7800: 0x20000000, + 0x8800: 0x20040000, + 0x9800: 0x0, + 0xa800: 0x21040080, + 0xb800: 0x1000080, + 0xc800: 0x20000080, + 0xd800: 0x21000000, + 0xe800: 0x1000000, + 0xf800: 0x40080, + 0x10000: 0x40000, + 0x11000: 0x80, + 0x12000: 0x20000000, + 0x13000: 0x21000080, + 0x14000: 0x1000080, + 0x15000: 0x21040000, + 0x16000: 0x20040080, + 0x17000: 0x1000000, + 0x18000: 0x21040080, + 0x19000: 0x21000000, + 0x1a000: 0x1040000, + 0x1b000: 0x20040000, + 0x1c000: 0x40080, + 0x1d000: 0x20000080, + 0x1e000: 0x0, + 0x1f000: 0x1040080, + 0x10800: 0x21000080, + 0x11800: 0x1000000, + 0x12800: 0x1040000, + 0x13800: 0x20040080, + 0x14800: 0x20000000, + 0x15800: 0x1040080, + 0x16800: 0x80, + 0x17800: 0x21040000, + 0x18800: 0x40080, + 0x19800: 0x21040080, + 0x1a800: 0x0, + 0x1b800: 0x21000000, + 0x1c800: 0x1000080, + 0x1d800: 0x40000, + 0x1e800: 0x20040000, + 0x1f800: 0x20000080 + }, + { + 0x0: 0x10000008, + 0x100: 0x2000, + 0x200: 0x10200000, + 0x300: 0x10202008, + 0x400: 0x10002000, + 0x500: 0x200000, + 0x600: 0x200008, + 0x700: 0x10000000, + 0x800: 0x0, + 0x900: 0x10002008, + 0xa00: 0x202000, + 0xb00: 0x8, + 0xc00: 0x10200008, + 0xd00: 0x202008, + 0xe00: 0x2008, + 0xf00: 0x10202000, + 0x80: 0x10200000, + 0x180: 0x10202008, + 0x280: 0x8, + 0x380: 0x200000, + 0x480: 0x202008, + 0x580: 0x10000008, + 0x680: 0x10002000, + 0x780: 0x2008, + 0x880: 0x200008, + 0x980: 0x2000, + 0xa80: 0x10002008, + 0xb80: 0x10200008, + 0xc80: 0x0, + 0xd80: 0x10202000, + 0xe80: 0x202000, + 0xf80: 0x10000000, + 0x1000: 0x10002000, + 0x1100: 0x10200008, + 0x1200: 0x10202008, + 0x1300: 0x2008, + 0x1400: 0x200000, + 0x1500: 0x10000000, + 0x1600: 0x10000008, + 0x1700: 0x202000, + 0x1800: 0x202008, + 0x1900: 0x0, + 0x1a00: 0x8, + 0x1b00: 0x10200000, + 0x1c00: 0x2000, + 0x1d00: 0x10002008, + 0x1e00: 0x10202000, + 0x1f00: 0x200008, + 0x1080: 0x8, + 0x1180: 0x202000, + 0x1280: 0x200000, + 0x1380: 0x10000008, + 0x1480: 0x10002000, + 0x1580: 0x2008, + 0x1680: 0x10202008, + 0x1780: 0x10200000, + 0x1880: 0x10202000, + 0x1980: 0x10200008, + 0x1a80: 0x2000, + 0x1b80: 0x202008, + 0x1c80: 0x200008, + 0x1d80: 0x0, + 0x1e80: 0x10000000, + 0x1f80: 0x10002008 + }, + { + 0x0: 0x100000, + 0x10: 0x2000401, + 0x20: 0x400, + 0x30: 0x100401, + 0x40: 0x2100401, + 0x50: 0x0, + 0x60: 0x1, + 0x70: 0x2100001, + 0x80: 0x2000400, + 0x90: 0x100001, + 0xa0: 0x2000001, + 0xb0: 0x2100400, + 0xc0: 0x2100000, + 0xd0: 0x401, + 0xe0: 0x100400, + 0xf0: 0x2000000, + 0x8: 0x2100001, + 0x18: 0x0, + 0x28: 0x2000401, + 0x38: 0x2100400, + 0x48: 0x100000, + 0x58: 0x2000001, + 0x68: 0x2000000, + 0x78: 0x401, + 0x88: 0x100401, + 0x98: 0x2000400, + 0xa8: 0x2100000, + 0xb8: 0x100001, + 0xc8: 0x400, + 0xd8: 0x2100401, + 0xe8: 0x1, + 0xf8: 0x100400, + 0x100: 0x2000000, + 0x110: 0x100000, + 0x120: 0x2000401, + 0x130: 0x2100001, + 0x140: 0x100001, + 0x150: 0x2000400, + 0x160: 0x2100400, + 0x170: 0x100401, + 0x180: 0x401, + 0x190: 0x2100401, + 0x1a0: 0x100400, + 0x1b0: 0x1, + 0x1c0: 0x0, + 0x1d0: 0x2100000, + 0x1e0: 0x2000001, + 0x1f0: 0x400, + 0x108: 0x100400, + 0x118: 0x2000401, + 0x128: 0x2100001, + 0x138: 0x1, + 0x148: 0x2000000, + 0x158: 0x100000, + 0x168: 0x401, + 0x178: 0x2100400, + 0x188: 0x2000001, + 0x198: 0x2100000, + 0x1a8: 0x0, + 0x1b8: 0x2100401, + 0x1c8: 0x100401, + 0x1d8: 0x400, + 0x1e8: 0x2000400, + 0x1f8: 0x100001 + }, + { + 0x0: 0x8000820, + 0x1: 0x20000, + 0x2: 0x8000000, + 0x3: 0x20, + 0x4: 0x20020, + 0x5: 0x8020820, + 0x6: 0x8020800, + 0x7: 0x800, + 0x8: 0x8020000, + 0x9: 0x8000800, + 0xa: 0x20800, + 0xb: 0x8020020, + 0xc: 0x820, + 0xd: 0x0, + 0xe: 0x8000020, + 0xf: 0x20820, + 0x80000000: 0x800, + 0x80000001: 0x8020820, + 0x80000002: 0x8000820, + 0x80000003: 0x8000000, + 0x80000004: 0x8020000, + 0x80000005: 0x20800, + 0x80000006: 0x20820, + 0x80000007: 0x20, + 0x80000008: 0x8000020, + 0x80000009: 0x820, + 0x8000000a: 0x20020, + 0x8000000b: 0x8020800, + 0x8000000c: 0x0, + 0x8000000d: 0x8020020, + 0x8000000e: 0x8000800, + 0x8000000f: 0x20000, + 0x10: 0x20820, + 0x11: 0x8020800, + 0x12: 0x20, + 0x13: 0x800, + 0x14: 0x8000800, + 0x15: 0x8000020, + 0x16: 0x8020020, + 0x17: 0x20000, + 0x18: 0x0, + 0x19: 0x20020, + 0x1a: 0x8020000, + 0x1b: 0x8000820, + 0x1c: 0x8020820, + 0x1d: 0x20800, + 0x1e: 0x820, + 0x1f: 0x8000000, + 0x80000010: 0x20000, + 0x80000011: 0x800, + 0x80000012: 0x8020020, + 0x80000013: 0x20820, + 0x80000014: 0x20, + 0x80000015: 0x8020000, + 0x80000016: 0x8000000, + 0x80000017: 0x8000820, + 0x80000018: 0x8020820, + 0x80000019: 0x8000020, + 0x8000001a: 0x8000800, + 0x8000001b: 0x0, + 0x8000001c: 0x20800, + 0x8000001d: 0x820, + 0x8000001e: 0x20020, + 0x8000001f: 0x8020800 + } + ]; + + // Masks that select the SBOX input + var SBOX_MASK = [ + 0xf8000001, 0x1f800000, 0x01f80000, 0x001f8000, + 0x0001f800, 0x00001f80, 0x000001f8, 0x8000001f + ]; + + /** + * DES block cipher algorithm. + */ + var DES = C_algo.DES = BlockCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + + // Select 56 bits according to PC1 + var keyBits = []; + for (var i = 0; i < 56; i++) { + var keyBitPos = PC1[i] - 1; + keyBits[i] = (keyWords[keyBitPos >>> 5] >>> (31 - keyBitPos % 32)) & 1; + } + + // Assemble 16 subkeys + var subKeys = this._subKeys = []; + for (var nSubKey = 0; nSubKey < 16; nSubKey++) { + // Create subkey + var subKey = subKeys[nSubKey] = []; + + // Shortcut + var bitShift = BIT_SHIFTS[nSubKey]; + + // Select 48 bits according to PC2 + for (var i = 0; i < 24; i++) { + // Select from the left 28 key bits + subKey[(i / 6) | 0] |= keyBits[((PC2[i] - 1) + bitShift) % 28] << (31 - i % 6); + + // Select from the right 28 key bits + subKey[4 + ((i / 6) | 0)] |= keyBits[28 + (((PC2[i + 24] - 1) + bitShift) % 28)] << (31 - i % 6); + } + + // Since each subkey is applied to an expanded 32-bit input, + // the subkey can be broken into 8 values scaled to 32-bits, + // which allows the key to be used without expansion + subKey[0] = (subKey[0] << 1) | (subKey[0] >>> 31); + for (var i = 1; i < 7; i++) { + subKey[i] = subKey[i] >>> ((i - 1) * 4 + 3); + } + subKey[7] = (subKey[7] << 5) | (subKey[7] >>> 27); + } + + // Compute inverse subkeys + var invSubKeys = this._invSubKeys = []; + for (var i = 0; i < 16; i++) { + invSubKeys[i] = subKeys[15 - i]; + } + }, + + encryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._subKeys); + }, + + decryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._invSubKeys); + }, + + _doCryptBlock: function (M, offset, subKeys) { + // Get input + this._lBlock = M[offset]; + this._rBlock = M[offset + 1]; + + // Initial permutation + exchangeLR.call(this, 4, 0x0f0f0f0f); + exchangeLR.call(this, 16, 0x0000ffff); + exchangeRL.call(this, 2, 0x33333333); + exchangeRL.call(this, 8, 0x00ff00ff); + exchangeLR.call(this, 1, 0x55555555); + + // Rounds + for (var round = 0; round < 16; round++) { + // Shortcuts + var subKey = subKeys[round]; + var lBlock = this._lBlock; + var rBlock = this._rBlock; + + // Feistel function + var f = 0; + for (var i = 0; i < 8; i++) { + f |= SBOX_P[i][((rBlock ^ subKey[i]) & SBOX_MASK[i]) >>> 0]; + } + this._lBlock = rBlock; + this._rBlock = lBlock ^ f; + } + + // Undo swap from last round + var t = this._lBlock; + this._lBlock = this._rBlock; + this._rBlock = t; + + // Final permutation + exchangeLR.call(this, 1, 0x55555555); + exchangeRL.call(this, 8, 0x00ff00ff); + exchangeRL.call(this, 2, 0x33333333); + exchangeLR.call(this, 16, 0x0000ffff); + exchangeLR.call(this, 4, 0x0f0f0f0f); + + // Set output + M[offset] = this._lBlock; + M[offset + 1] = this._rBlock; + }, + + keySize: 64/32, + + ivSize: 64/32, + + blockSize: 64/32 + }); + + // Swap bits across the left and right words + function exchangeLR(offset, mask) { + var t = ((this._lBlock >>> offset) ^ this._rBlock) & mask; + this._rBlock ^= t; + this._lBlock ^= t << offset; + } + + function exchangeRL(offset, mask) { + var t = ((this._rBlock >>> offset) ^ this._lBlock) & mask; + this._lBlock ^= t; + this._rBlock ^= t << offset; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.DES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.DES.decrypt(ciphertext, key, cfg); + */ + C.DES = BlockCipher._createHelper(DES); + + /** + * Triple-DES block cipher algorithm. + */ + var TripleDES = C_algo.TripleDES = BlockCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + // Make sure the key length is valid (64, 128 or >= 192 bit) + if (keyWords.length !== 2 && keyWords.length !== 4 && keyWords.length < 6) { + throw new Error('Invalid key length - 3DES requires the key length to be 64, 128, 192 or >192.'); + } + + // Extend the key according to the keying options defined in 3DES standard + var key1 = keyWords.slice(0, 2); + var key2 = keyWords.length < 4 ? keyWords.slice(0, 2) : keyWords.slice(2, 4); + var key3 = keyWords.length < 6 ? keyWords.slice(0, 2) : keyWords.slice(4, 6); + + // Create DES instances + this._des1 = DES.createEncryptor(WordArray.create(key1)); + this._des2 = DES.createEncryptor(WordArray.create(key2)); + this._des3 = DES.createEncryptor(WordArray.create(key3)); + }, + + encryptBlock: function (M, offset) { + this._des1.encryptBlock(M, offset); + this._des2.decryptBlock(M, offset); + this._des3.encryptBlock(M, offset); + }, + + decryptBlock: function (M, offset) { + this._des3.decryptBlock(M, offset); + this._des2.encryptBlock(M, offset); + this._des1.decryptBlock(M, offset); + }, + + keySize: 192/32, + + ivSize: 64/32, + + blockSize: 64/32 + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.TripleDES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.TripleDES.decrypt(ciphertext, key, cfg); + */ + C.TripleDES = BlockCipher._createHelper(TripleDES); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + /** + * RC4 stream cipher algorithm. + */ + var RC4 = C_algo.RC4 = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + var keySigBytes = key.sigBytes; + + // Init sbox + var S = this._S = []; + for (var i = 0; i < 256; i++) { + S[i] = i; + } + + // Key setup + for (var i = 0, j = 0; i < 256; i++) { + var keyByteIndex = i % keySigBytes; + var keyByte = (keyWords[keyByteIndex >>> 2] >>> (24 - (keyByteIndex % 4) * 8)) & 0xff; + + j = (j + S[i] + keyByte) % 256; + + // Swap + var t = S[i]; + S[i] = S[j]; + S[j] = t; + } + + // Counters + this._i = this._j = 0; + }, + + _doProcessBlock: function (M, offset) { + M[offset] ^= generateKeystreamWord.call(this); + }, + + keySize: 256/32, + + ivSize: 0 + }); + + function generateKeystreamWord() { + // Shortcuts + var S = this._S; + var i = this._i; + var j = this._j; + + // Generate keystream word + var keystreamWord = 0; + for (var n = 0; n < 4; n++) { + i = (i + 1) % 256; + j = (j + S[i]) % 256; + + // Swap + var t = S[i]; + S[i] = S[j]; + S[j] = t; + + keystreamWord |= S[(S[i] + S[j]) % 256] << (24 - n * 8); + } + + // Update counters + this._i = i; + this._j = j; + + return keystreamWord; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4.decrypt(ciphertext, key, cfg); + */ + C.RC4 = StreamCipher._createHelper(RC4); + + /** + * Modified RC4 stream cipher algorithm. + */ + var RC4Drop = C_algo.RC4Drop = RC4.extend({ + /** + * Configuration options. + * + * @property {number} drop The number of keystream words to drop. Default 192 + */ + cfg: RC4.cfg.extend({ + drop: 192 + }), + + _doReset: function () { + RC4._doReset.call(this); + + // Drop + for (var i = this.cfg.drop; i > 0; i--) { + generateKeystreamWord.call(this); + } + } + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4Drop.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4Drop.decrypt(ciphertext, key, cfg); + */ + C.RC4Drop = StreamCipher._createHelper(RC4Drop); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + // Reusable objects + var S = []; + var C_ = []; + var G = []; + + /** + * Rabbit stream cipher algorithm + */ + var Rabbit = C_algo.Rabbit = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var K = this._key.words; + var iv = this.cfg.iv; + + // Swap endian + for (var i = 0; i < 4; i++) { + K[i] = (((K[i] << 8) | (K[i] >>> 24)) & 0x00ff00ff) | + (((K[i] << 24) | (K[i] >>> 8)) & 0xff00ff00); + } + + // Generate initial state values + var X = this._X = [ + K[0], (K[3] << 16) | (K[2] >>> 16), + K[1], (K[0] << 16) | (K[3] >>> 16), + K[2], (K[1] << 16) | (K[0] >>> 16), + K[3], (K[2] << 16) | (K[1] >>> 16) + ]; + + // Generate initial counter values + var C = this._C = [ + (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff), + (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff), + (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff), + (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff) + ]; + + // Carry bit + this._b = 0; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + + // Modify the counters + for (var i = 0; i < 8; i++) { + C[i] ^= X[(i + 4) & 7]; + } + + // IV setup + if (iv) { + // Shortcuts + var IV = iv.words; + var IV_0 = IV[0]; + var IV_1 = IV[1]; + + // Generate four subvectors + var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00); + var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00); + var i1 = (i0 >>> 16) | (i2 & 0xffff0000); + var i3 = (i2 << 16) | (i0 & 0x0000ffff); + + // Modify counter values + C[0] ^= i0; + C[1] ^= i1; + C[2] ^= i2; + C[3] ^= i3; + C[4] ^= i0; + C[5] ^= i1; + C[6] ^= i2; + C[7] ^= i3; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + } + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var X = this._X; + + // Iterate the system + nextState.call(this); + + // Generate four keystream words + S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16); + S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16); + S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16); + S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16); + + for (var i = 0; i < 4; i++) { + // Swap endian + S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) | + (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00); + + // Encrypt + M[offset + i] ^= S[i]; + } + }, + + blockSize: 128/32, + + ivSize: 64/32 + }); + + function nextState() { + // Shortcuts + var X = this._X; + var C = this._C; + + // Save old counter values + for (var i = 0; i < 8; i++) { + C_[i] = C[i]; + } + + // Calculate new counter values + C[0] = (C[0] + 0x4d34d34d + this._b) | 0; + C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0; + C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0; + C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0; + C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0; + C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0; + C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0; + C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0; + this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0; + + // Calculate the g-values + for (var i = 0; i < 8; i++) { + var gx = X[i] + C[i]; + + // Construct high and low argument for squaring + var ga = gx & 0xffff; + var gb = gx >>> 16; + + // Calculate high and low result of squaring + var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb; + var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0); + + // High XOR low + G[i] = gh ^ gl; + } + + // Calculate new state values + X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0; + X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0; + X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0; + X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0; + X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0; + X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0; + X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0; + X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.Rabbit.encrypt(message, key, cfg); + * var plaintext = CryptoJS.Rabbit.decrypt(ciphertext, key, cfg); + */ + C.Rabbit = StreamCipher._createHelper(Rabbit); + }()); + + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + // Reusable objects + var S = []; + var C_ = []; + var G = []; + + /** + * Rabbit stream cipher algorithm. + * + * This is a legacy version that neglected to convert the key to little-endian. + * This error doesn't affect the cipher's security, + * but it does affect its compatibility with other implementations. + */ + var RabbitLegacy = C_algo.RabbitLegacy = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var K = this._key.words; + var iv = this.cfg.iv; + + // Generate initial state values + var X = this._X = [ + K[0], (K[3] << 16) | (K[2] >>> 16), + K[1], (K[0] << 16) | (K[3] >>> 16), + K[2], (K[1] << 16) | (K[0] >>> 16), + K[3], (K[2] << 16) | (K[1] >>> 16) + ]; + + // Generate initial counter values + var C = this._C = [ + (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff), + (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff), + (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff), + (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff) + ]; + + // Carry bit + this._b = 0; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + + // Modify the counters + for (var i = 0; i < 8; i++) { + C[i] ^= X[(i + 4) & 7]; + } + + // IV setup + if (iv) { + // Shortcuts + var IV = iv.words; + var IV_0 = IV[0]; + var IV_1 = IV[1]; + + // Generate four subvectors + var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00); + var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00); + var i1 = (i0 >>> 16) | (i2 & 0xffff0000); + var i3 = (i2 << 16) | (i0 & 0x0000ffff); + + // Modify counter values + C[0] ^= i0; + C[1] ^= i1; + C[2] ^= i2; + C[3] ^= i3; + C[4] ^= i0; + C[5] ^= i1; + C[6] ^= i2; + C[7] ^= i3; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + } + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var X = this._X; + + // Iterate the system + nextState.call(this); + + // Generate four keystream words + S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16); + S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16); + S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16); + S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16); + + for (var i = 0; i < 4; i++) { + // Swap endian + S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) | + (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00); + + // Encrypt + M[offset + i] ^= S[i]; + } + }, + + blockSize: 128/32, + + ivSize: 64/32 + }); + + function nextState() { + // Shortcuts + var X = this._X; + var C = this._C; + + // Save old counter values + for (var i = 0; i < 8; i++) { + C_[i] = C[i]; + } + + // Calculate new counter values + C[0] = (C[0] + 0x4d34d34d + this._b) | 0; + C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0; + C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0; + C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0; + C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0; + C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0; + C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0; + C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0; + this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0; + + // Calculate the g-values + for (var i = 0; i < 8; i++) { + var gx = X[i] + C[i]; + + // Construct high and low argument for squaring + var ga = gx & 0xffff; + var gb = gx >>> 16; + + // Calculate high and low result of squaring + var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb; + var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0); + + // High XOR low + G[i] = gh ^ gl; + } + + // Calculate new state values + X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0; + X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0; + X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0; + X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0; + X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0; + X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0; + X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0; + X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RabbitLegacy.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RabbitLegacy.decrypt(ciphertext, key, cfg); + */ + C.RabbitLegacy = StreamCipher._createHelper(RabbitLegacy); + }()); + + + return CryptoJS; + +})); diff --git a/module/static/aes/crypto-js-master/docs/QuickStartGuide.wiki b/module/static/aes/crypto-js-master/docs/QuickStartGuide.wiki new file mode 100644 index 0000000..7c7adbc --- /dev/null +++ b/module/static/aes/crypto-js-master/docs/QuickStartGuide.wiki @@ -0,0 +1,470 @@ + + +---- + += Quick-start Guide = + +== Hashers == + +=== The Hasher Algorithms === + +==== MD5 ==== + +MD5 is a widely used hash function. It's been used in a variety of security applications and is also commonly used to check the integrity of files. Though, MD5 is not collision resistant, and it isn't suitable for applications like SSL certificates or digital signatures that rely on this property. + +{{{ + + +}}} + +==== SHA-1 ==== + +The SHA hash functions were designed by the National Security Agency (NSA). SHA-1 is the most established of the existing SHA hash functions, and it's used in a variety of security applications and protocols. Though, SHA-1's collision resistance has been weakening as new attacks are discovered or improved. + +{{{ + + +}}} + +==== SHA-2 ==== + +SHA-256 is one of the four variants in the SHA-2 set. It isn't as widely used as SHA-1, though it appears to provide much better security. + +{{{ + + +}}} + +SHA-512 is largely identical to SHA-256 but operates on 64-bit words rather than 32. + +{{{ + + +}}} + +CryptoJS also supports SHA-224 and SHA-384, which are largely identical but truncated versions of SHA-256 and SHA-512 respectively. + +==== SHA-3 ==== + +SHA-3 is the winner of a five-year competition to select a new cryptographic hash algorithm where 64 competing designs were evaluated. + +{{{ + + +}}} + +SHA-3 can be configured to output hash lengths of one of 224, 256, 384, or 512 bits. The default is 512 bits. + +{{{ + + +}}} + +==== RIPEMD-160 ==== + +{{{ + + +}}} + +=== The Hasher Input === + +The hash algorithms accept either strings or instances of CryptoJS.lib.WordArray. A WordArray object represents an array of 32-bit words. When you pass a string, it's automatically converted to a WordArray encoded as UTF-8. + +=== The Hasher Output === + +The hash you get back isn't a string yet. It's a WordArray object. When you use a WordArray object in a string context, it's automatically converted to a hex string. + +{{{ + + +}}} + +You can convert a WordArray object to other formats by explicitly calling the toString method and passing an encoder. + +{{{ + + + +}}} + +=== Progressive Hashing === + +{{{ + + +}}} + +== HMAC == + +Keyed-hash message authentication codes (HMAC) is a mechanism for message authentication using cryptographic hash functions. + +HMAC can be used in combination with any iterated cryptographic hash function. + +{{{ + + + + + +}}} + +=== Progressive HMAC Hashing === + +{{{ + + +}}} + +== PBKDF2 == + +PBKDF2 is a password-based key derivation function. In many applications of cryptography, user security is ultimately dependent on a password, and because a password usually can't be used directly as a cryptographic key, some processing is required. + +A salt provides a large set of keys for any given password, and an iteration count increases the cost of producing keys from a password, thereby also increasing the difficulty of attack. + +{{{ + + +}}} + +== Ciphers == + +=== The Cipher Algorithms === + +==== AES ==== + +The Advanced Encryption Standard (AES) is a U.S. Federal Information Processing Standard (FIPS). It was selected after a 5-year process where 15 competing designs were evaluated. + +{{{ + + +}}} + +CryptoJS supports AES-128, AES-192, and AES-256. It will pick the variant by the size of the key you pass in. If you use a passphrase, then it will generate a 256-bit key. + +==== DES, Triple DES ==== + +DES is a previously dominant algorithm for encryption, and was published as an official Federal Information Processing Standard (FIPS). DES is now considered to be insecure due to the small key size. + +{{{ + + +}}} + +Triple DES applies DES three times to each block to increase the key size. The algorithm is believed to be secure in this form. + +{{{ + + +}}} + +==== Rabbit ==== + +Rabbit is a high-performance stream cipher and a finalist in the eSTREAM Portfolio. It is one of the four designs selected after a 3 1/2-year process where 22 designs were evaluated. + +{{{ + + +}}} + +==== RC4, RC4Drop ==== + +RC4 is a widely-used stream cipher. It's used in popular protocols such as SSL and WEP. Although remarkable for its simplicity and speed, the algorithm's history doesn't inspire confidence in its security. + +{{{ + + +}}} + +It was discovered that the first few bytes of keystream are strongly non-random and leak information about the key. We can defend against this attack by discarding the initial portion of the keystream. This modified algorithm is traditionally called RC4-drop. + +By default, 192 words (768 bytes) are dropped, but you can configure the algorithm to drop any number of words. + +{{{ + + +}}} + +=== Custom Key and IV === + +{{{ + + +}}} + +=== Block Modes and Padding === + +{{{ + + + + +}}} + +CryptoJS supports the following modes: + + * CBC (the default) + * CFB + * CTR + * OFB + * ECB + +And CryptoJS supports the following padding schemes: + + * Pkcs7 (the default) + * Iso97971 + * AnsiX923 + * Iso10126 + * ZeroPadding + * NoPadding + +=== The Cipher Input === + +For the plaintext message, the cipher algorithms accept either strings or instances of CryptoJS.lib.WordArray. + +For the key, when you pass a string, it's treated as a passphrase and used to derive an actual key and IV. Or you can pass a WordArray that represents the actual key. If you pass the actual key, you must also pass the actual IV. + +For the ciphertext, the cipher algorithms accept either strings or instances of CryptoJS.lib.CipherParams. A CipherParams object represents a collection of parameters such as the IV, a salt, and the raw ciphertext itself. When you pass a string, it's automatically converted to a CipherParams object according to a configurable format strategy. + +=== The Cipher Output === + +The plaintext you get back after decryption is a WordArray object. See Hashers' Output for more detail. + +The ciphertext you get back after encryption isn't a string yet. It's a CipherParams object. A CipherParams object gives you access to all the parameters used during encryption. When you use a CipherParams object in a string context, it's automatically converted to a string according to a format strategy. The default is an OpenSSL-compatible format. + +{{{ + + +}}} + +You can define your own formats in order to be compatible with other crypto implementations. A format is an object with two methods—stringify and parse—that converts between CipherParams objects and ciphertext strings. + +Here's how you might write a JSON formatter: + +{{{ + + +}}} + +=== Progressive Ciphering === + +{{{ + + +}}} + +=== Interoperability === + +==== With OpenSSL ==== + +Encrypt with OpenSSL: + +{{{ +openssl enc -aes-256-cbc -in infile -out outfile -pass pass:"Secret Passphrase" -e -base64 +}}} + +Decrypt with CryptoJS: + +{{{ + + +}}} + +== Encoders == + +CryptoJS can convert from encoding formats such as Base64, Latin1 or Hex to WordArray objects and vica versa. + +{{{ + + + + +}}} diff --git a/module/static/aes/crypto-js-master/enc-base64.js b/module/static/aes/crypto-js-master/enc-base64.js new file mode 100644 index 0000000..152ca33 --- /dev/null +++ b/module/static/aes/crypto-js-master/enc-base64.js @@ -0,0 +1,136 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * Base64 encoding strategy. + */ + var Base64 = C_enc.Base64 = { + /** + * Converts a word array to a Base64 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Base64 string. + * + * @static + * + * @example + * + * var base64String = CryptoJS.enc.Base64.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var map = this._map; + + // Clamp excess bits + wordArray.clamp(); + + // Convert + var base64Chars = []; + for (var i = 0; i < sigBytes; i += 3) { + var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + + var triplet = (byte1 << 16) | (byte2 << 8) | byte3; + + for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { + base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); + } + } + + // Add padding + var paddingChar = map.charAt(64); + if (paddingChar) { + while (base64Chars.length % 4) { + base64Chars.push(paddingChar); + } + } + + return base64Chars.join(''); + }, + + /** + * Converts a Base64 string to a word array. + * + * @param {string} base64Str The Base64 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Base64.parse(base64String); + */ + parse: function (base64Str) { + // Shortcuts + var base64StrLength = base64Str.length; + var map = this._map; + var reverseMap = this._reverseMap; + + if (!reverseMap) { + reverseMap = this._reverseMap = []; + for (var j = 0; j < map.length; j++) { + reverseMap[map.charCodeAt(j)] = j; + } + } + + // Ignore padding + var paddingChar = map.charAt(64); + if (paddingChar) { + var paddingIndex = base64Str.indexOf(paddingChar); + if (paddingIndex !== -1) { + base64StrLength = paddingIndex; + } + } + + // Convert + return parseLoop(base64Str, base64StrLength, reverseMap); + + }, + + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + }; + + function parseLoop(base64Str, base64StrLength, reverseMap) { + var words = []; + var nBytes = 0; + for (var i = 0; i < base64StrLength; i++) { + if (i % 4) { + var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); + var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); + var bitsCombined = bits1 | bits2; + words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); + nBytes++; + } + } + return WordArray.create(words, nBytes); + } + }()); + + + return CryptoJS.enc.Base64; + +})); diff --git a/module/static/aes/crypto-js-master/enc-base64url.js b/module/static/aes/crypto-js-master/enc-base64url.js new file mode 100644 index 0000000..9ffcc95 --- /dev/null +++ b/module/static/aes/crypto-js-master/enc-base64url.js @@ -0,0 +1,140 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * Base64url encoding strategy. + */ + var Base64url = C_enc.Base64url = { + /** + * Converts a word array to a Base64url string. + * + * @param {WordArray} wordArray The word array. + * + * @param {boolean} urlSafe Whether to use url safe + * + * @return {string} The Base64url string. + * + * @static + * + * @example + * + * var base64String = CryptoJS.enc.Base64url.stringify(wordArray); + */ + stringify: function (wordArray, urlSafe=true) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var map = urlSafe ? this._safe_map : this._map; + + // Clamp excess bits + wordArray.clamp(); + + // Convert + var base64Chars = []; + for (var i = 0; i < sigBytes; i += 3) { + var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + + var triplet = (byte1 << 16) | (byte2 << 8) | byte3; + + for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { + base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); + } + } + + // Add padding + var paddingChar = map.charAt(64); + if (paddingChar) { + while (base64Chars.length % 4) { + base64Chars.push(paddingChar); + } + } + + return base64Chars.join(''); + }, + + /** + * Converts a Base64url string to a word array. + * + * @param {string} base64Str The Base64url string. + * + * @param {boolean} urlSafe Whether to use url safe + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Base64url.parse(base64String); + */ + parse: function (base64Str, urlSafe=true) { + // Shortcuts + var base64StrLength = base64Str.length; + var map = urlSafe ? this._safe_map : this._map; + var reverseMap = this._reverseMap; + + if (!reverseMap) { + reverseMap = this._reverseMap = []; + for (var j = 0; j < map.length; j++) { + reverseMap[map.charCodeAt(j)] = j; + } + } + + // Ignore padding + var paddingChar = map.charAt(64); + if (paddingChar) { + var paddingIndex = base64Str.indexOf(paddingChar); + if (paddingIndex !== -1) { + base64StrLength = paddingIndex; + } + } + + // Convert + return parseLoop(base64Str, base64StrLength, reverseMap); + + }, + + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', + _safe_map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + }; + + function parseLoop(base64Str, base64StrLength, reverseMap) { + var words = []; + var nBytes = 0; + for (var i = 0; i < base64StrLength; i++) { + if (i % 4) { + var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); + var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); + var bitsCombined = bits1 | bits2; + words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); + nBytes++; + } + } + return WordArray.create(words, nBytes); + } + }()); + + return CryptoJS.enc.Base64url; + +})); diff --git a/module/static/aes/crypto-js-master/enc-hex.js b/module/static/aes/crypto-js-master/enc-hex.js new file mode 100644 index 0000000..daef773 --- /dev/null +++ b/module/static/aes/crypto-js-master/enc-hex.js @@ -0,0 +1,18 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.enc.Hex; + +})); diff --git a/module/static/aes/crypto-js-master/enc-latin1.js b/module/static/aes/crypto-js-master/enc-latin1.js new file mode 100644 index 0000000..4991a9a --- /dev/null +++ b/module/static/aes/crypto-js-master/enc-latin1.js @@ -0,0 +1,18 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.enc.Latin1; + +})); diff --git a/module/static/aes/crypto-js-master/enc-utf16.js b/module/static/aes/crypto-js-master/enc-utf16.js new file mode 100644 index 0000000..585911a --- /dev/null +++ b/module/static/aes/crypto-js-master/enc-utf16.js @@ -0,0 +1,149 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * UTF-16 BE encoding strategy. + */ + var Utf16BE = C_enc.Utf16 = C_enc.Utf16BE = { + /** + * Converts a word array to a UTF-16 BE string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-16 BE string. + * + * @static + * + * @example + * + * var utf16String = CryptoJS.enc.Utf16.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var utf16Chars = []; + for (var i = 0; i < sigBytes; i += 2) { + var codePoint = (words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff; + utf16Chars.push(String.fromCharCode(codePoint)); + } + + return utf16Chars.join(''); + }, + + /** + * Converts a UTF-16 BE string to a word array. + * + * @param {string} utf16Str The UTF-16 BE string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf16.parse(utf16String); + */ + parse: function (utf16Str) { + // Shortcut + var utf16StrLength = utf16Str.length; + + // Convert + var words = []; + for (var i = 0; i < utf16StrLength; i++) { + words[i >>> 1] |= utf16Str.charCodeAt(i) << (16 - (i % 2) * 16); + } + + return WordArray.create(words, utf16StrLength * 2); + } + }; + + /** + * UTF-16 LE encoding strategy. + */ + C_enc.Utf16LE = { + /** + * Converts a word array to a UTF-16 LE string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-16 LE string. + * + * @static + * + * @example + * + * var utf16Str = CryptoJS.enc.Utf16LE.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var utf16Chars = []; + for (var i = 0; i < sigBytes; i += 2) { + var codePoint = swapEndian((words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff); + utf16Chars.push(String.fromCharCode(codePoint)); + } + + return utf16Chars.join(''); + }, + + /** + * Converts a UTF-16 LE string to a word array. + * + * @param {string} utf16Str The UTF-16 LE string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf16LE.parse(utf16Str); + */ + parse: function (utf16Str) { + // Shortcut + var utf16StrLength = utf16Str.length; + + // Convert + var words = []; + for (var i = 0; i < utf16StrLength; i++) { + words[i >>> 1] |= swapEndian(utf16Str.charCodeAt(i) << (16 - (i % 2) * 16)); + } + + return WordArray.create(words, utf16StrLength * 2); + } + }; + + function swapEndian(word) { + return ((word << 8) & 0xff00ff00) | ((word >>> 8) & 0x00ff00ff); + } + }()); + + + return CryptoJS.enc.Utf16; + +})); diff --git a/module/static/aes/crypto-js-master/enc-utf8.js b/module/static/aes/crypto-js-master/enc-utf8.js new file mode 100644 index 0000000..51e6a4a --- /dev/null +++ b/module/static/aes/crypto-js-master/enc-utf8.js @@ -0,0 +1,18 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.enc.Utf8; + +})); diff --git a/module/static/aes/crypto-js-master/evpkdf.js b/module/static/aes/crypto-js-master/evpkdf.js new file mode 100644 index 0000000..1c50a42 --- /dev/null +++ b/module/static/aes/crypto-js-master/evpkdf.js @@ -0,0 +1,134 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./sha1"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./sha1", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var MD5 = C_algo.MD5; + + /** + * This key derivation function is meant to conform with EVP_BytesToKey. + * www.openssl.org/docs/crypto/EVP_BytesToKey.html + */ + var EvpKDF = C_algo.EvpKDF = Base.extend({ + /** + * Configuration options. + * + * @property {number} keySize The key size in words to generate. Default: 4 (128 bits) + * @property {Hasher} hasher The hash algorithm to use. Default: MD5 + * @property {number} iterations The number of iterations to perform. Default: 1 + */ + cfg: Base.extend({ + keySize: 128/32, + hasher: MD5, + iterations: 1 + }), + + /** + * Initializes a newly created key derivation function. + * + * @param {Object} cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.EvpKDF.create(); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8, iterations: 1000 }); + */ + init: function (cfg) { + this.cfg = this.cfg.extend(cfg); + }, + + /** + * Derives a key from a password. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * + * @return {WordArray} The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute: function (password, salt) { + var block; + + // Shortcut + var cfg = this.cfg; + + // Init hasher + var hasher = cfg.hasher.create(); + + // Initial values + var derivedKey = WordArray.create(); + + // Shortcuts + var derivedKeyWords = derivedKey.words; + var keySize = cfg.keySize; + var iterations = cfg.iterations; + + // Generate key + while (derivedKeyWords.length < keySize) { + if (block) { + hasher.update(block); + } + block = hasher.update(password).finalize(salt); + hasher.reset(); + + // Iterations + for (var i = 1; i < iterations; i++) { + block = hasher.finalize(block); + hasher.reset(); + } + + derivedKey.concat(block); + } + derivedKey.sigBytes = keySize * 4; + + return derivedKey; + } + }); + + /** + * Derives a key from a password. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * @param {Object} cfg (Optional) The configuration options to use for this computation. + * + * @return {WordArray} The derived key. + * + * @static + * + * @example + * + * var key = CryptoJS.EvpKDF(password, salt); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8 }); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8, iterations: 1000 }); + */ + C.EvpKDF = function (password, salt, cfg) { + return EvpKDF.create(cfg).compute(password, salt); + }; + }()); + + + return CryptoJS.EvpKDF; + +})); diff --git a/module/static/aes/crypto-js-master/format-hex.js b/module/static/aes/crypto-js-master/format-hex.js new file mode 100644 index 0000000..69e0a6b --- /dev/null +++ b/module/static/aes/crypto-js-master/format-hex.js @@ -0,0 +1,66 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var CipherParams = C_lib.CipherParams; + var C_enc = C.enc; + var Hex = C_enc.Hex; + var C_format = C.format; + + var HexFormatter = C_format.Hex = { + /** + * Converts the ciphertext of a cipher params object to a hexadecimally encoded string. + * + * @param {CipherParams} cipherParams The cipher params object. + * + * @return {string} The hexadecimally encoded string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.format.Hex.stringify(cipherParams); + */ + stringify: function (cipherParams) { + return cipherParams.ciphertext.toString(Hex); + }, + + /** + * Converts a hexadecimally encoded ciphertext string to a cipher params object. + * + * @param {string} input The hexadecimally encoded string. + * + * @return {CipherParams} The cipher params object. + * + * @static + * + * @example + * + * var cipherParams = CryptoJS.format.Hex.parse(hexString); + */ + parse: function (input) { + var ciphertext = Hex.parse(input); + return CipherParams.create({ ciphertext: ciphertext }); + } + }; + }()); + + + return CryptoJS.format.Hex; + +})); diff --git a/module/static/aes/crypto-js-master/format-openssl.js b/module/static/aes/crypto-js-master/format-openssl.js new file mode 100644 index 0000000..5ee4a45 --- /dev/null +++ b/module/static/aes/crypto-js-master/format-openssl.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.format.OpenSSL; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-md5.js b/module/static/aes/crypto-js-master/hmac-md5.js new file mode 100644 index 0000000..72b10c4 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-md5.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./md5"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./md5", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacMD5; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-ripemd160.js b/module/static/aes/crypto-js-master/hmac-ripemd160.js new file mode 100644 index 0000000..7f39618 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-ripemd160.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./ripemd160"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./ripemd160", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacRIPEMD160; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-sha1.js b/module/static/aes/crypto-js-master/hmac-sha1.js new file mode 100644 index 0000000..0e266da --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-sha1.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./sha1"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./sha1", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacSHA1; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-sha224.js b/module/static/aes/crypto-js-master/hmac-sha224.js new file mode 100644 index 0000000..e6514d7 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-sha224.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./sha256"), require("./sha224"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./sha256", "./sha224", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacSHA224; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-sha256.js b/module/static/aes/crypto-js-master/hmac-sha256.js new file mode 100644 index 0000000..e91a612 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-sha256.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./sha256"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./sha256", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacSHA256; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-sha3.js b/module/static/aes/crypto-js-master/hmac-sha3.js new file mode 100644 index 0000000..9a2be02 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-sha3.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./x64-core"), require("./sha3"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./x64-core", "./sha3", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacSHA3; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-sha384.js b/module/static/aes/crypto-js-master/hmac-sha384.js new file mode 100644 index 0000000..0e2f3e5 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-sha384.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./x64-core"), require("./sha512"), require("./sha384"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./x64-core", "./sha512", "./sha384", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacSHA384; + +})); diff --git a/module/static/aes/crypto-js-master/hmac-sha512.js b/module/static/aes/crypto-js-master/hmac-sha512.js new file mode 100644 index 0000000..bc77d30 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac-sha512.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./x64-core"), require("./sha512"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./x64-core", "./sha512", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.HmacSHA512; + +})); diff --git a/module/static/aes/crypto-js-master/hmac.js b/module/static/aes/crypto-js-master/hmac.js new file mode 100644 index 0000000..ee04ff5 --- /dev/null +++ b/module/static/aes/crypto-js-master/hmac.js @@ -0,0 +1,143 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var C_algo = C.algo; + + /** + * HMAC algorithm. + */ + var HMAC = C_algo.HMAC = Base.extend({ + /** + * Initializes a newly created HMAC. + * + * @param {Hasher} hasher The hash algorithm to use. + * @param {WordArray|string} key The secret key. + * + * @example + * + * var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key); + */ + init: function (hasher, key) { + // Init hasher + hasher = this._hasher = new hasher.init(); + + // Convert string to WordArray, else assume WordArray already + if (typeof key == 'string') { + key = Utf8.parse(key); + } + + // Shortcuts + var hasherBlockSize = hasher.blockSize; + var hasherBlockSizeBytes = hasherBlockSize * 4; + + // Allow arbitrary length keys + if (key.sigBytes > hasherBlockSizeBytes) { + key = hasher.finalize(key); + } + + // Clamp excess bits + key.clamp(); + + // Clone key for inner and outer pads + var oKey = this._oKey = key.clone(); + var iKey = this._iKey = key.clone(); + + // Shortcuts + var oKeyWords = oKey.words; + var iKeyWords = iKey.words; + + // XOR keys with pad constants + for (var i = 0; i < hasherBlockSize; i++) { + oKeyWords[i] ^= 0x5c5c5c5c; + iKeyWords[i] ^= 0x36363636; + } + oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes; + + // Set initial values + this.reset(); + }, + + /** + * Resets this HMAC to its initial state. + * + * @example + * + * hmacHasher.reset(); + */ + reset: function () { + // Shortcut + var hasher = this._hasher; + + // Reset + hasher.reset(); + hasher.update(this._iKey); + }, + + /** + * Updates this HMAC with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {HMAC} This HMAC instance. + * + * @example + * + * hmacHasher.update('message'); + * hmacHasher.update(wordArray); + */ + update: function (messageUpdate) { + this._hasher.update(messageUpdate); + + // Chainable + return this; + }, + + /** + * Finalizes the HMAC computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The HMAC. + * + * @example + * + * var hmac = hmacHasher.finalize(); + * var hmac = hmacHasher.finalize('message'); + * var hmac = hmacHasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Shortcut + var hasher = this._hasher; + + // Compute HMAC + var innerHash = hasher.finalize(messageUpdate); + hasher.reset(); + var hmac = hasher.finalize(this._oKey.clone().concat(innerHash)); + + return hmac; + } + }); + }()); + + +})); diff --git a/module/static/aes/crypto-js-master/index.js b/module/static/aes/crypto-js-master/index.js new file mode 100644 index 0000000..a7be5d1 --- /dev/null +++ b/module/static/aes/crypto-js-master/index.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./x64-core"), require("./lib-typedarrays"), require("./enc-utf16"), require("./enc-base64"), require("./enc-base64url"), require("./md5"), require("./sha1"), require("./sha256"), require("./sha224"), require("./sha512"), require("./sha384"), require("./sha3"), require("./ripemd160"), require("./hmac"), require("./pbkdf2"), require("./evpkdf"), require("./cipher-core"), require("./mode-cfb"), require("./mode-ctr"), require("./mode-ctr-gladman"), require("./mode-ofb"), require("./mode-ecb"), require("./pad-ansix923"), require("./pad-iso10126"), require("./pad-iso97971"), require("./pad-zeropadding"), require("./pad-nopadding"), require("./format-hex"), require("./aes"), require("./tripledes"), require("./rc4"), require("./rabbit"), require("./rabbit-legacy")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./x64-core", "./lib-typedarrays", "./enc-utf16", "./enc-base64", "./enc-base64url", "./md5", "./sha1", "./sha256", "./sha224", "./sha512", "./sha384", "./sha3", "./ripemd160", "./hmac", "./pbkdf2", "./evpkdf", "./cipher-core", "./mode-cfb", "./mode-ctr", "./mode-ctr-gladman", "./mode-ofb", "./mode-ecb", "./pad-ansix923", "./pad-iso10126", "./pad-iso97971", "./pad-zeropadding", "./pad-nopadding", "./format-hex", "./aes", "./tripledes", "./rc4", "./rabbit", "./rabbit-legacy"], factory); + } + else { + // Global (browser) + root.CryptoJS = factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS; + +})); diff --git a/module/static/aes/crypto-js-master/lib-typedarrays.js b/module/static/aes/crypto-js-master/lib-typedarrays.js new file mode 100644 index 0000000..be49a40 --- /dev/null +++ b/module/static/aes/crypto-js-master/lib-typedarrays.js @@ -0,0 +1,76 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Check if typed arrays are supported + if (typeof ArrayBuffer != 'function') { + return; + } + + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + + // Reference original init + var superInit = WordArray.init; + + // Augment WordArray.init to handle typed arrays + var subInit = WordArray.init = function (typedArray) { + // Convert buffers to uint8 + if (typedArray instanceof ArrayBuffer) { + typedArray = new Uint8Array(typedArray); + } + + // Convert other array views to uint8 + if ( + typedArray instanceof Int8Array || + (typeof Uint8ClampedArray !== "undefined" && typedArray instanceof Uint8ClampedArray) || + typedArray instanceof Int16Array || + typedArray instanceof Uint16Array || + typedArray instanceof Int32Array || + typedArray instanceof Uint32Array || + typedArray instanceof Float32Array || + typedArray instanceof Float64Array + ) { + typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); + } + + // Handle Uint8Array + if (typedArray instanceof Uint8Array) { + // Shortcut + var typedArrayByteLength = typedArray.byteLength; + + // Extract bytes + var words = []; + for (var i = 0; i < typedArrayByteLength; i++) { + words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8); + } + + // Initialize this word array + superInit.call(this, words, typedArrayByteLength); + } else { + // Else call normal init + superInit.apply(this, arguments); + } + }; + + subInit.prototype = WordArray; + }()); + + + return CryptoJS.lib.WordArray; + +})); diff --git a/module/static/aes/crypto-js-master/md5.js b/module/static/aes/crypto-js-master/md5.js new file mode 100644 index 0000000..f78592a --- /dev/null +++ b/module/static/aes/crypto-js-master/md5.js @@ -0,0 +1,268 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Constants table + var T = []; + + // Compute constants + (function () { + for (var i = 0; i < 64; i++) { + T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0; + } + }()); + + /** + * MD5 hash algorithm. + */ + var MD5 = C_algo.MD5 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Swap endian + for (var i = 0; i < 16; i++) { + // Shortcuts + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + + // Shortcuts + var H = this._hash.words; + + var M_offset_0 = M[offset + 0]; + var M_offset_1 = M[offset + 1]; + var M_offset_2 = M[offset + 2]; + var M_offset_3 = M[offset + 3]; + var M_offset_4 = M[offset + 4]; + var M_offset_5 = M[offset + 5]; + var M_offset_6 = M[offset + 6]; + var M_offset_7 = M[offset + 7]; + var M_offset_8 = M[offset + 8]; + var M_offset_9 = M[offset + 9]; + var M_offset_10 = M[offset + 10]; + var M_offset_11 = M[offset + 11]; + var M_offset_12 = M[offset + 12]; + var M_offset_13 = M[offset + 13]; + var M_offset_14 = M[offset + 14]; + var M_offset_15 = M[offset + 15]; + + // Working varialbes + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + + // Computation + a = FF(a, b, c, d, M_offset_0, 7, T[0]); + d = FF(d, a, b, c, M_offset_1, 12, T[1]); + c = FF(c, d, a, b, M_offset_2, 17, T[2]); + b = FF(b, c, d, a, M_offset_3, 22, T[3]); + a = FF(a, b, c, d, M_offset_4, 7, T[4]); + d = FF(d, a, b, c, M_offset_5, 12, T[5]); + c = FF(c, d, a, b, M_offset_6, 17, T[6]); + b = FF(b, c, d, a, M_offset_7, 22, T[7]); + a = FF(a, b, c, d, M_offset_8, 7, T[8]); + d = FF(d, a, b, c, M_offset_9, 12, T[9]); + c = FF(c, d, a, b, M_offset_10, 17, T[10]); + b = FF(b, c, d, a, M_offset_11, 22, T[11]); + a = FF(a, b, c, d, M_offset_12, 7, T[12]); + d = FF(d, a, b, c, M_offset_13, 12, T[13]); + c = FF(c, d, a, b, M_offset_14, 17, T[14]); + b = FF(b, c, d, a, M_offset_15, 22, T[15]); + + a = GG(a, b, c, d, M_offset_1, 5, T[16]); + d = GG(d, a, b, c, M_offset_6, 9, T[17]); + c = GG(c, d, a, b, M_offset_11, 14, T[18]); + b = GG(b, c, d, a, M_offset_0, 20, T[19]); + a = GG(a, b, c, d, M_offset_5, 5, T[20]); + d = GG(d, a, b, c, M_offset_10, 9, T[21]); + c = GG(c, d, a, b, M_offset_15, 14, T[22]); + b = GG(b, c, d, a, M_offset_4, 20, T[23]); + a = GG(a, b, c, d, M_offset_9, 5, T[24]); + d = GG(d, a, b, c, M_offset_14, 9, T[25]); + c = GG(c, d, a, b, M_offset_3, 14, T[26]); + b = GG(b, c, d, a, M_offset_8, 20, T[27]); + a = GG(a, b, c, d, M_offset_13, 5, T[28]); + d = GG(d, a, b, c, M_offset_2, 9, T[29]); + c = GG(c, d, a, b, M_offset_7, 14, T[30]); + b = GG(b, c, d, a, M_offset_12, 20, T[31]); + + a = HH(a, b, c, d, M_offset_5, 4, T[32]); + d = HH(d, a, b, c, M_offset_8, 11, T[33]); + c = HH(c, d, a, b, M_offset_11, 16, T[34]); + b = HH(b, c, d, a, M_offset_14, 23, T[35]); + a = HH(a, b, c, d, M_offset_1, 4, T[36]); + d = HH(d, a, b, c, M_offset_4, 11, T[37]); + c = HH(c, d, a, b, M_offset_7, 16, T[38]); + b = HH(b, c, d, a, M_offset_10, 23, T[39]); + a = HH(a, b, c, d, M_offset_13, 4, T[40]); + d = HH(d, a, b, c, M_offset_0, 11, T[41]); + c = HH(c, d, a, b, M_offset_3, 16, T[42]); + b = HH(b, c, d, a, M_offset_6, 23, T[43]); + a = HH(a, b, c, d, M_offset_9, 4, T[44]); + d = HH(d, a, b, c, M_offset_12, 11, T[45]); + c = HH(c, d, a, b, M_offset_15, 16, T[46]); + b = HH(b, c, d, a, M_offset_2, 23, T[47]); + + a = II(a, b, c, d, M_offset_0, 6, T[48]); + d = II(d, a, b, c, M_offset_7, 10, T[49]); + c = II(c, d, a, b, M_offset_14, 15, T[50]); + b = II(b, c, d, a, M_offset_5, 21, T[51]); + a = II(a, b, c, d, M_offset_12, 6, T[52]); + d = II(d, a, b, c, M_offset_3, 10, T[53]); + c = II(c, d, a, b, M_offset_10, 15, T[54]); + b = II(b, c, d, a, M_offset_1, 21, T[55]); + a = II(a, b, c, d, M_offset_8, 6, T[56]); + d = II(d, a, b, c, M_offset_15, 10, T[57]); + c = II(c, d, a, b, M_offset_6, 15, T[58]); + b = II(b, c, d, a, M_offset_13, 21, T[59]); + a = II(a, b, c, d, M_offset_4, 6, T[60]); + d = II(d, a, b, c, M_offset_11, 10, T[61]); + c = II(c, d, a, b, M_offset_2, 15, T[62]); + b = II(b, c, d, a, M_offset_9, 21, T[63]); + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + + var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000); + var nBitsTotalL = nBitsTotal; + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ( + (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | + (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00) + ); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | + (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00) + ); + + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 4; i++) { + // Shortcut + var H_i = H[i]; + + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + function FF(a, b, c, d, x, s, t) { + var n = a + ((b & c) | (~b & d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function GG(a, b, c, d, x, s, t) { + var n = a + ((b & d) | (c & ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function HH(a, b, c, d, x, s, t) { + var n = a + (b ^ c ^ d) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function II(a, b, c, d, x, s, t) { + var n = a + (c ^ (b | ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.MD5('message'); + * var hash = CryptoJS.MD5(wordArray); + */ + C.MD5 = Hasher._createHelper(MD5); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacMD5(message, key); + */ + C.HmacMD5 = Hasher._createHmacHelper(MD5); + }(Math)); + + + return CryptoJS.MD5; + +})); diff --git a/module/static/aes/crypto-js-master/mode-cfb.js b/module/static/aes/crypto-js-master/mode-cfb.js new file mode 100644 index 0000000..b030840 --- /dev/null +++ b/module/static/aes/crypto-js-master/mode-cfb.js @@ -0,0 +1,80 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * Cipher Feedback block mode. + */ + CryptoJS.mode.CFB = (function () { + var CFB = CryptoJS.lib.BlockCipherMode.extend(); + + CFB.Encryptor = CFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher); + + // Remember this block to use with next block + this._prevBlock = words.slice(offset, offset + blockSize); + } + }); + + CFB.Decryptor = CFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // Remember this block to use with next block + var thisBlock = words.slice(offset, offset + blockSize); + + generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher); + + // This block becomes the previous block + this._prevBlock = thisBlock; + } + }); + + function generateKeystreamAndEncrypt(words, offset, blockSize, cipher) { + var keystream; + + // Shortcut + var iv = this._iv; + + // Generate keystream + if (iv) { + keystream = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } else { + keystream = this._prevBlock; + } + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + + return CFB; + }()); + + + return CryptoJS.mode.CFB; + +})); diff --git a/module/static/aes/crypto-js-master/mode-ctr-gladman.js b/module/static/aes/crypto-js-master/mode-ctr-gladman.js new file mode 100644 index 0000000..9640129 --- /dev/null +++ b/module/static/aes/crypto-js-master/mode-ctr-gladman.js @@ -0,0 +1,116 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** @preserve + * Counter block mode compatible with Dr Brian Gladman fileenc.c + * derived from CryptoJS.mode.CTR + * Jan Hruby jhruby.web@gmail.com + */ + CryptoJS.mode.CTRGladman = (function () { + var CTRGladman = CryptoJS.lib.BlockCipherMode.extend(); + + function incWord(word) + { + if (((word >> 24) & 0xff) === 0xff) { //overflow + var b1 = (word >> 16)&0xff; + var b2 = (word >> 8)&0xff; + var b3 = word & 0xff; + + if (b1 === 0xff) // overflow b1 + { + b1 = 0; + if (b2 === 0xff) + { + b2 = 0; + if (b3 === 0xff) + { + b3 = 0; + } + else + { + ++b3; + } + } + else + { + ++b2; + } + } + else + { + ++b1; + } + + word = 0; + word += (b1 << 16); + word += (b2 << 8); + word += b3; + } + else + { + word += (0x01 << 24); + } + return word; + } + + function incCounter(counter) + { + if ((counter[0] = incWord(counter[0])) === 0) + { + // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8 + counter[1] = incWord(counter[1]); + } + return counter; + } + + var Encryptor = CTRGladman.Encryptor = CTRGladman.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var counter = this._counter; + + // Generate keystream + if (iv) { + counter = this._counter = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + + incCounter(counter); + + var keystream = counter.slice(0); + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + CTRGladman.Decryptor = Encryptor; + + return CTRGladman; + }()); + + + + + return CryptoJS.mode.CTRGladman; + +})); diff --git a/module/static/aes/crypto-js-master/mode-ctr.js b/module/static/aes/crypto-js-master/mode-ctr.js new file mode 100644 index 0000000..e4073ac --- /dev/null +++ b/module/static/aes/crypto-js-master/mode-ctr.js @@ -0,0 +1,58 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * Counter block mode. + */ + CryptoJS.mode.CTR = (function () { + var CTR = CryptoJS.lib.BlockCipherMode.extend(); + + var Encryptor = CTR.Encryptor = CTR.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var counter = this._counter; + + // Generate keystream + if (iv) { + counter = this._counter = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + var keystream = counter.slice(0); + cipher.encryptBlock(keystream, 0); + + // Increment counter + counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0 + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + CTR.Decryptor = Encryptor; + + return CTR; + }()); + + + return CryptoJS.mode.CTR; + +})); diff --git a/module/static/aes/crypto-js-master/mode-ecb.js b/module/static/aes/crypto-js-master/mode-ecb.js new file mode 100644 index 0000000..d44ea73 --- /dev/null +++ b/module/static/aes/crypto-js-master/mode-ecb.js @@ -0,0 +1,40 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * Electronic Codebook block mode. + */ + CryptoJS.mode.ECB = (function () { + var ECB = CryptoJS.lib.BlockCipherMode.extend(); + + ECB.Encryptor = ECB.extend({ + processBlock: function (words, offset) { + this._cipher.encryptBlock(words, offset); + } + }); + + ECB.Decryptor = ECB.extend({ + processBlock: function (words, offset) { + this._cipher.decryptBlock(words, offset); + } + }); + + return ECB; + }()); + + + return CryptoJS.mode.ECB; + +})); diff --git a/module/static/aes/crypto-js-master/mode-ofb.js b/module/static/aes/crypto-js-master/mode-ofb.js new file mode 100644 index 0000000..b0417df --- /dev/null +++ b/module/static/aes/crypto-js-master/mode-ofb.js @@ -0,0 +1,54 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * Output Feedback block mode. + */ + CryptoJS.mode.OFB = (function () { + var OFB = CryptoJS.lib.BlockCipherMode.extend(); + + var Encryptor = OFB.Encryptor = OFB.extend({ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher + var blockSize = cipher.blockSize; + var iv = this._iv; + var keystream = this._keystream; + + // Generate keystream + if (iv) { + keystream = this._keystream = iv.slice(0); + + // Remove IV for subsequent blocks + this._iv = undefined; + } + cipher.encryptBlock(keystream, 0); + + // Encrypt + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= keystream[i]; + } + } + }); + + OFB.Decryptor = Encryptor; + + return OFB; + }()); + + + return CryptoJS.mode.OFB; + +})); diff --git a/module/static/aes/crypto-js-master/package.json b/module/static/aes/crypto-js-master/package.json new file mode 100644 index 0000000..76bb388 --- /dev/null +++ b/module/static/aes/crypto-js-master/package.json @@ -0,0 +1,42 @@ +{ + "name": "crypto-js", + "version": "4.1.1", + "description": "JavaScript library of crypto standards.", + "license": "MIT", + "author": { + "name": "Evan Vosberg", + "url": "http://github.com/evanvosberg" + }, + "homepage": "http://github.com/brix/crypto-js", + "repository": { + "type": "git", + "url": "http://github.com/brix/crypto-js.git" + }, + "keywords": [ + "security", + "crypto", + "Hash", + "MD5", + "SHA1", + "SHA-1", + "SHA256", + "SHA-256", + "RC4", + "Rabbit", + "AES", + "DES", + "PBKDF2", + "HMAC", + "OFB", + "CFB", + "CTR", + "CBC", + "Base64", + "Base64url" + ], + "main": "index.js", + "dependencies": {}, + "browser": { + "crypto": false + } +} diff --git a/module/static/aes/crypto-js-master/pad-ansix923.js b/module/static/aes/crypto-js-master/pad-ansix923.js new file mode 100644 index 0000000..c29618e --- /dev/null +++ b/module/static/aes/crypto-js-master/pad-ansix923.js @@ -0,0 +1,49 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * ANSI X.923 padding strategy. + */ + CryptoJS.pad.AnsiX923 = { + pad: function (data, blockSize) { + // Shortcuts + var dataSigBytes = data.sigBytes; + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - dataSigBytes % blockSizeBytes; + + // Compute last byte position + var lastBytePos = dataSigBytes + nPaddingBytes - 1; + + // Pad + data.clamp(); + data.words[lastBytePos >>> 2] |= nPaddingBytes << (24 - (lastBytePos % 4) * 8); + data.sigBytes += nPaddingBytes; + }, + + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + + return CryptoJS.pad.Ansix923; + +})); diff --git a/module/static/aes/crypto-js-master/pad-iso10126.js b/module/static/aes/crypto-js-master/pad-iso10126.js new file mode 100644 index 0000000..d6bc3cc --- /dev/null +++ b/module/static/aes/crypto-js-master/pad-iso10126.js @@ -0,0 +1,44 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * ISO 10126 padding strategy. + */ + CryptoJS.pad.Iso10126 = { + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Pad + data.concat(CryptoJS.lib.WordArray.random(nPaddingBytes - 1)). + concat(CryptoJS.lib.WordArray.create([nPaddingBytes << 24], 1)); + }, + + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + + return CryptoJS.pad.Iso10126; + +})); diff --git a/module/static/aes/crypto-js-master/pad-iso97971.js b/module/static/aes/crypto-js-master/pad-iso97971.js new file mode 100644 index 0000000..2da3694 --- /dev/null +++ b/module/static/aes/crypto-js-master/pad-iso97971.js @@ -0,0 +1,40 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * ISO/IEC 9797-1 Padding Method 2. + */ + CryptoJS.pad.Iso97971 = { + pad: function (data, blockSize) { + // Add 0x80 byte + data.concat(CryptoJS.lib.WordArray.create([0x80000000], 1)); + + // Zero pad the rest + CryptoJS.pad.ZeroPadding.pad(data, blockSize); + }, + + unpad: function (data) { + // Remove zero padding + CryptoJS.pad.ZeroPadding.unpad(data); + + // Remove one more byte -- the 0x80 byte + data.sigBytes--; + } + }; + + + return CryptoJS.pad.Iso97971; + +})); diff --git a/module/static/aes/crypto-js-master/pad-nopadding.js b/module/static/aes/crypto-js-master/pad-nopadding.js new file mode 100644 index 0000000..dd0103d --- /dev/null +++ b/module/static/aes/crypto-js-master/pad-nopadding.js @@ -0,0 +1,30 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * A noop padding strategy. + */ + CryptoJS.pad.NoPadding = { + pad: function () { + }, + + unpad: function () { + } + }; + + + return CryptoJS.pad.NoPadding; + +})); diff --git a/module/static/aes/crypto-js-master/pad-pkcs7.js b/module/static/aes/crypto-js-master/pad-pkcs7.js new file mode 100644 index 0000000..97078d2 --- /dev/null +++ b/module/static/aes/crypto-js-master/pad-pkcs7.js @@ -0,0 +1,18 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + return CryptoJS.pad.Pkcs7; + +})); diff --git a/module/static/aes/crypto-js-master/pad-zeropadding.js b/module/static/aes/crypto-js-master/pad-zeropadding.js new file mode 100644 index 0000000..f342e11 --- /dev/null +++ b/module/static/aes/crypto-js-master/pad-zeropadding.js @@ -0,0 +1,47 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** + * Zero padding strategy. + */ + CryptoJS.pad.ZeroPadding = { + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Pad + data.clamp(); + data.sigBytes += blockSizeBytes - ((data.sigBytes % blockSizeBytes) || blockSizeBytes); + }, + + unpad: function (data) { + // Shortcut + var dataWords = data.words; + + // Unpad + var i = data.sigBytes - 1; + for (var i = data.sigBytes - 1; i >= 0; i--) { + if (((dataWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff)) { + data.sigBytes = i + 1; + break; + } + } + } + }; + + + return CryptoJS.pad.ZeroPadding; + +})); diff --git a/module/static/aes/crypto-js-master/pbkdf2.js b/module/static/aes/crypto-js-master/pbkdf2.js new file mode 100644 index 0000000..0a490d7 --- /dev/null +++ b/module/static/aes/crypto-js-master/pbkdf2.js @@ -0,0 +1,145 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./sha1"), require("./hmac")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./sha1", "./hmac"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var SHA1 = C_algo.SHA1; + var HMAC = C_algo.HMAC; + + /** + * Password-Based Key Derivation Function 2 algorithm. + */ + var PBKDF2 = C_algo.PBKDF2 = Base.extend({ + /** + * Configuration options. + * + * @property {number} keySize The key size in words to generate. Default: 4 (128 bits) + * @property {Hasher} hasher The hasher to use. Default: SHA1 + * @property {number} iterations The number of iterations to perform. Default: 1 + */ + cfg: Base.extend({ + keySize: 128/32, + hasher: SHA1, + iterations: 1 + }), + + /** + * Initializes a newly created key derivation function. + * + * @param {Object} cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.PBKDF2.create(); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 }); + */ + init: function (cfg) { + this.cfg = this.cfg.extend(cfg); + }, + + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * + * @return {WordArray} The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute: function (password, salt) { + // Shortcut + var cfg = this.cfg; + + // Init HMAC + var hmac = HMAC.create(cfg.hasher, password); + + // Initial values + var derivedKey = WordArray.create(); + var blockIndex = WordArray.create([0x00000001]); + + // Shortcuts + var derivedKeyWords = derivedKey.words; + var blockIndexWords = blockIndex.words; + var keySize = cfg.keySize; + var iterations = cfg.iterations; + + // Generate key + while (derivedKeyWords.length < keySize) { + var block = hmac.update(salt).finalize(blockIndex); + hmac.reset(); + + // Shortcuts + var blockWords = block.words; + var blockWordsLength = blockWords.length; + + // Iterations + var intermediate = block; + for (var i = 1; i < iterations; i++) { + intermediate = hmac.finalize(intermediate); + hmac.reset(); + + // Shortcut + var intermediateWords = intermediate.words; + + // XOR intermediate with block + for (var j = 0; j < blockWordsLength; j++) { + blockWords[j] ^= intermediateWords[j]; + } + } + + derivedKey.concat(block); + blockIndexWords[0]++; + } + derivedKey.sigBytes = keySize * 4; + + return derivedKey; + } + }); + + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param {WordArray|string} password The password. + * @param {WordArray|string} salt A salt. + * @param {Object} cfg (Optional) The configuration options to use for this computation. + * + * @return {WordArray} The derived key. + * + * @static + * + * @example + * + * var key = CryptoJS.PBKDF2(password, salt); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 }); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 }); + */ + C.PBKDF2 = function (password, salt, cfg) { + return PBKDF2.create(cfg).compute(password, salt); + }; + }()); + + + return CryptoJS.PBKDF2; + +})); diff --git a/module/static/aes/crypto-js-master/rabbit-legacy.js b/module/static/aes/crypto-js-master/rabbit-legacy.js new file mode 100644 index 0000000..73a25bb --- /dev/null +++ b/module/static/aes/crypto-js-master/rabbit-legacy.js @@ -0,0 +1,190 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./enc-base64"), require("./md5"), require("./evpkdf"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./enc-base64", "./md5", "./evpkdf", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + // Reusable objects + var S = []; + var C_ = []; + var G = []; + + /** + * Rabbit stream cipher algorithm. + * + * This is a legacy version that neglected to convert the key to little-endian. + * This error doesn't affect the cipher's security, + * but it does affect its compatibility with other implementations. + */ + var RabbitLegacy = C_algo.RabbitLegacy = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var K = this._key.words; + var iv = this.cfg.iv; + + // Generate initial state values + var X = this._X = [ + K[0], (K[3] << 16) | (K[2] >>> 16), + K[1], (K[0] << 16) | (K[3] >>> 16), + K[2], (K[1] << 16) | (K[0] >>> 16), + K[3], (K[2] << 16) | (K[1] >>> 16) + ]; + + // Generate initial counter values + var C = this._C = [ + (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff), + (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff), + (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff), + (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff) + ]; + + // Carry bit + this._b = 0; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + + // Modify the counters + for (var i = 0; i < 8; i++) { + C[i] ^= X[(i + 4) & 7]; + } + + // IV setup + if (iv) { + // Shortcuts + var IV = iv.words; + var IV_0 = IV[0]; + var IV_1 = IV[1]; + + // Generate four subvectors + var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00); + var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00); + var i1 = (i0 >>> 16) | (i2 & 0xffff0000); + var i3 = (i2 << 16) | (i0 & 0x0000ffff); + + // Modify counter values + C[0] ^= i0; + C[1] ^= i1; + C[2] ^= i2; + C[3] ^= i3; + C[4] ^= i0; + C[5] ^= i1; + C[6] ^= i2; + C[7] ^= i3; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + } + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var X = this._X; + + // Iterate the system + nextState.call(this); + + // Generate four keystream words + S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16); + S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16); + S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16); + S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16); + + for (var i = 0; i < 4; i++) { + // Swap endian + S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) | + (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00); + + // Encrypt + M[offset + i] ^= S[i]; + } + }, + + blockSize: 128/32, + + ivSize: 64/32 + }); + + function nextState() { + // Shortcuts + var X = this._X; + var C = this._C; + + // Save old counter values + for (var i = 0; i < 8; i++) { + C_[i] = C[i]; + } + + // Calculate new counter values + C[0] = (C[0] + 0x4d34d34d + this._b) | 0; + C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0; + C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0; + C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0; + C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0; + C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0; + C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0; + C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0; + this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0; + + // Calculate the g-values + for (var i = 0; i < 8; i++) { + var gx = X[i] + C[i]; + + // Construct high and low argument for squaring + var ga = gx & 0xffff; + var gb = gx >>> 16; + + // Calculate high and low result of squaring + var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb; + var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0); + + // High XOR low + G[i] = gh ^ gl; + } + + // Calculate new state values + X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0; + X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0; + X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0; + X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0; + X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0; + X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0; + X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0; + X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RabbitLegacy.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RabbitLegacy.decrypt(ciphertext, key, cfg); + */ + C.RabbitLegacy = StreamCipher._createHelper(RabbitLegacy); + }()); + + + return CryptoJS.RabbitLegacy; + +})); diff --git a/module/static/aes/crypto-js-master/rabbit.js b/module/static/aes/crypto-js-master/rabbit.js new file mode 100644 index 0000000..80b0e9d --- /dev/null +++ b/module/static/aes/crypto-js-master/rabbit.js @@ -0,0 +1,192 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./enc-base64"), require("./md5"), require("./evpkdf"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./enc-base64", "./md5", "./evpkdf", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + // Reusable objects + var S = []; + var C_ = []; + var G = []; + + /** + * Rabbit stream cipher algorithm + */ + var Rabbit = C_algo.Rabbit = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var K = this._key.words; + var iv = this.cfg.iv; + + // Swap endian + for (var i = 0; i < 4; i++) { + K[i] = (((K[i] << 8) | (K[i] >>> 24)) & 0x00ff00ff) | + (((K[i] << 24) | (K[i] >>> 8)) & 0xff00ff00); + } + + // Generate initial state values + var X = this._X = [ + K[0], (K[3] << 16) | (K[2] >>> 16), + K[1], (K[0] << 16) | (K[3] >>> 16), + K[2], (K[1] << 16) | (K[0] >>> 16), + K[3], (K[2] << 16) | (K[1] >>> 16) + ]; + + // Generate initial counter values + var C = this._C = [ + (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff), + (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff), + (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff), + (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff) + ]; + + // Carry bit + this._b = 0; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + + // Modify the counters + for (var i = 0; i < 8; i++) { + C[i] ^= X[(i + 4) & 7]; + } + + // IV setup + if (iv) { + // Shortcuts + var IV = iv.words; + var IV_0 = IV[0]; + var IV_1 = IV[1]; + + // Generate four subvectors + var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00); + var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00); + var i1 = (i0 >>> 16) | (i2 & 0xffff0000); + var i3 = (i2 << 16) | (i0 & 0x0000ffff); + + // Modify counter values + C[0] ^= i0; + C[1] ^= i1; + C[2] ^= i2; + C[3] ^= i3; + C[4] ^= i0; + C[5] ^= i1; + C[6] ^= i2; + C[7] ^= i3; + + // Iterate the system four times + for (var i = 0; i < 4; i++) { + nextState.call(this); + } + } + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var X = this._X; + + // Iterate the system + nextState.call(this); + + // Generate four keystream words + S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16); + S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16); + S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16); + S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16); + + for (var i = 0; i < 4; i++) { + // Swap endian + S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) | + (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00); + + // Encrypt + M[offset + i] ^= S[i]; + } + }, + + blockSize: 128/32, + + ivSize: 64/32 + }); + + function nextState() { + // Shortcuts + var X = this._X; + var C = this._C; + + // Save old counter values + for (var i = 0; i < 8; i++) { + C_[i] = C[i]; + } + + // Calculate new counter values + C[0] = (C[0] + 0x4d34d34d + this._b) | 0; + C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0; + C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0; + C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0; + C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0; + C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0; + C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0; + C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0; + this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0; + + // Calculate the g-values + for (var i = 0; i < 8; i++) { + var gx = X[i] + C[i]; + + // Construct high and low argument for squaring + var ga = gx & 0xffff; + var gb = gx >>> 16; + + // Calculate high and low result of squaring + var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb; + var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0); + + // High XOR low + G[i] = gh ^ gl; + } + + // Calculate new state values + X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0; + X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0; + X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0; + X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0; + X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0; + X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0; + X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0; + X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.Rabbit.encrypt(message, key, cfg); + * var plaintext = CryptoJS.Rabbit.decrypt(ciphertext, key, cfg); + */ + C.Rabbit = StreamCipher._createHelper(Rabbit); + }()); + + + return CryptoJS.Rabbit; + +})); diff --git a/module/static/aes/crypto-js-master/rc4.js b/module/static/aes/crypto-js-master/rc4.js new file mode 100644 index 0000000..d81a58c --- /dev/null +++ b/module/static/aes/crypto-js-master/rc4.js @@ -0,0 +1,139 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./enc-base64"), require("./md5"), require("./evpkdf"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./enc-base64", "./md5", "./evpkdf", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var StreamCipher = C_lib.StreamCipher; + var C_algo = C.algo; + + /** + * RC4 stream cipher algorithm. + */ + var RC4 = C_algo.RC4 = StreamCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + var keySigBytes = key.sigBytes; + + // Init sbox + var S = this._S = []; + for (var i = 0; i < 256; i++) { + S[i] = i; + } + + // Key setup + for (var i = 0, j = 0; i < 256; i++) { + var keyByteIndex = i % keySigBytes; + var keyByte = (keyWords[keyByteIndex >>> 2] >>> (24 - (keyByteIndex % 4) * 8)) & 0xff; + + j = (j + S[i] + keyByte) % 256; + + // Swap + var t = S[i]; + S[i] = S[j]; + S[j] = t; + } + + // Counters + this._i = this._j = 0; + }, + + _doProcessBlock: function (M, offset) { + M[offset] ^= generateKeystreamWord.call(this); + }, + + keySize: 256/32, + + ivSize: 0 + }); + + function generateKeystreamWord() { + // Shortcuts + var S = this._S; + var i = this._i; + var j = this._j; + + // Generate keystream word + var keystreamWord = 0; + for (var n = 0; n < 4; n++) { + i = (i + 1) % 256; + j = (j + S[i]) % 256; + + // Swap + var t = S[i]; + S[i] = S[j]; + S[j] = t; + + keystreamWord |= S[(S[i] + S[j]) % 256] << (24 - n * 8); + } + + // Update counters + this._i = i; + this._j = j; + + return keystreamWord; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4.decrypt(ciphertext, key, cfg); + */ + C.RC4 = StreamCipher._createHelper(RC4); + + /** + * Modified RC4 stream cipher algorithm. + */ + var RC4Drop = C_algo.RC4Drop = RC4.extend({ + /** + * Configuration options. + * + * @property {number} drop The number of keystream words to drop. Default 192 + */ + cfg: RC4.cfg.extend({ + drop: 192 + }), + + _doReset: function () { + RC4._doReset.call(this); + + // Drop + for (var i = this.cfg.drop; i > 0; i--) { + generateKeystreamWord.call(this); + } + } + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4Drop.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4Drop.decrypt(ciphertext, key, cfg); + */ + C.RC4Drop = StreamCipher._createHelper(RC4Drop); + }()); + + + return CryptoJS.RC4; + +})); diff --git a/module/static/aes/crypto-js-master/ripemd160.js b/module/static/aes/crypto-js-master/ripemd160.js new file mode 100644 index 0000000..022f175 --- /dev/null +++ b/module/static/aes/crypto-js-master/ripemd160.js @@ -0,0 +1,267 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + /** @preserve + (c) 2012 by Cédric Mesnil. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Constants table + var _zl = WordArray.create([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13]); + var _zr = WordArray.create([ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11]); + var _sl = WordArray.create([ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ]); + var _sr = WordArray.create([ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ]); + + var _hl = WordArray.create([ 0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]); + var _hr = WordArray.create([ 0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000]); + + /** + * RIPEMD160 hash algorithm. + */ + var RIPEMD160 = C_algo.RIPEMD160 = Hasher.extend({ + _doReset: function () { + this._hash = WordArray.create([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]); + }, + + _doProcessBlock: function (M, offset) { + + // Swap endian + for (var i = 0; i < 16; i++) { + // Shortcuts + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + // Swap + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + // Shortcut + var H = this._hash.words; + var hl = _hl.words; + var hr = _hr.words; + var zl = _zl.words; + var zr = _zr.words; + var sl = _sl.words; + var sr = _sr.words; + + // Working variables + var al, bl, cl, dl, el; + var ar, br, cr, dr, er; + + ar = al = H[0]; + br = bl = H[1]; + cr = cl = H[2]; + dr = dl = H[3]; + er = el = H[4]; + // Computation + var t; + for (var i = 0; i < 80; i += 1) { + t = (al + M[offset+zl[i]])|0; + if (i<16){ + t += f1(bl,cl,dl) + hl[0]; + } else if (i<32) { + t += f2(bl,cl,dl) + hl[1]; + } else if (i<48) { + t += f3(bl,cl,dl) + hl[2]; + } else if (i<64) { + t += f4(bl,cl,dl) + hl[3]; + } else {// if (i<80) { + t += f5(bl,cl,dl) + hl[4]; + } + t = t|0; + t = rotl(t,sl[i]); + t = (t+el)|0; + al = el; + el = dl; + dl = rotl(cl, 10); + cl = bl; + bl = t; + + t = (ar + M[offset+zr[i]])|0; + if (i<16){ + t += f5(br,cr,dr) + hr[0]; + } else if (i<32) { + t += f4(br,cr,dr) + hr[1]; + } else if (i<48) { + t += f3(br,cr,dr) + hr[2]; + } else if (i<64) { + t += f2(br,cr,dr) + hr[3]; + } else {// if (i<80) { + t += f1(br,cr,dr) + hr[4]; + } + t = t|0; + t = rotl(t,sr[i]) ; + t = (t+er)|0; + ar = er; + er = dr; + dr = rotl(cr, 10); + cr = br; + br = t; + } + // Intermediate hash value + t = (H[1] + cl + dr)|0; + H[1] = (H[2] + dl + er)|0; + H[2] = (H[3] + el + ar)|0; + H[3] = (H[4] + al + br)|0; + H[4] = (H[0] + bl + cr)|0; + H[0] = t; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotal << 8) | (nBitsTotal >>> 24)) & 0x00ff00ff) | + (((nBitsTotal << 24) | (nBitsTotal >>> 8)) & 0xff00ff00) + ); + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 5; i++) { + // Shortcut + var H_i = H[i]; + + // Swap + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + + function f1(x, y, z) { + return ((x) ^ (y) ^ (z)); + + } + + function f2(x, y, z) { + return (((x)&(y)) | ((~x)&(z))); + } + + function f3(x, y, z) { + return (((x) | (~(y))) ^ (z)); + } + + function f4(x, y, z) { + return (((x) & (z)) | ((y)&(~(z)))); + } + + function f5(x, y, z) { + return ((x) ^ ((y) |(~(z)))); + + } + + function rotl(x,n) { + return (x<>>(32-n)); + } + + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.RIPEMD160('message'); + * var hash = CryptoJS.RIPEMD160(wordArray); + */ + C.RIPEMD160 = Hasher._createHelper(RIPEMD160); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacRIPEMD160(message, key); + */ + C.HmacRIPEMD160 = Hasher._createHmacHelper(RIPEMD160); + }(Math)); + + + return CryptoJS.RIPEMD160; + +})); diff --git a/module/static/aes/crypto-js-master/sha1.js b/module/static/aes/crypto-js-master/sha1.js new file mode 100644 index 0000000..8568bb3 --- /dev/null +++ b/module/static/aes/crypto-js-master/sha1.js @@ -0,0 +1,150 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Reusable object + var W = []; + + /** + * SHA-1 hash algorithm. + */ + var SHA1 = C_algo.SHA1 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476, + 0xc3d2e1f0 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + + // Computation + for (var i = 0; i < 80; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; + W[i] = (n << 1) | (n >>> 31); + } + + var t = ((a << 5) | (a >>> 27)) + e + W[i]; + if (i < 20) { + t += ((b & c) | (~b & d)) + 0x5a827999; + } else if (i < 40) { + t += (b ^ c ^ d) + 0x6ed9eba1; + } else if (i < 60) { + t += ((b & c) | (b & d) | (c & d)) - 0x70e44324; + } else /* if (i < 80) */ { + t += (b ^ c ^ d) - 0x359d3e2a; + } + + e = d; + d = c; + c = (b << 30) | (b >>> 2); + b = a; + a = t; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA1('message'); + * var hash = CryptoJS.SHA1(wordArray); + */ + C.SHA1 = Hasher._createHelper(SHA1); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA1(message, key); + */ + C.HmacSHA1 = Hasher._createHmacHelper(SHA1); + }()); + + + return CryptoJS.SHA1; + +})); diff --git a/module/static/aes/crypto-js-master/sha224.js b/module/static/aes/crypto-js-master/sha224.js new file mode 100644 index 0000000..b831ae5 --- /dev/null +++ b/module/static/aes/crypto-js-master/sha224.js @@ -0,0 +1,80 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./sha256")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./sha256"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_algo = C.algo; + var SHA256 = C_algo.SHA256; + + /** + * SHA-224 hash algorithm. + */ + var SHA224 = C_algo.SHA224 = SHA256.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, + 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4 + ]); + }, + + _doFinalize: function () { + var hash = SHA256._doFinalize.call(this); + + hash.sigBytes -= 4; + + return hash; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA224('message'); + * var hash = CryptoJS.SHA224(wordArray); + */ + C.SHA224 = SHA256._createHelper(SHA224); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA224(message, key); + */ + C.HmacSHA224 = SHA256._createHmacHelper(SHA224); + }()); + + + return CryptoJS.SHA224; + +})); diff --git a/module/static/aes/crypto-js-master/sha256.js b/module/static/aes/crypto-js-master/sha256.js new file mode 100644 index 0000000..a8a4965 --- /dev/null +++ b/module/static/aes/crypto-js-master/sha256.js @@ -0,0 +1,199 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Initialization and round constants tables + var H = []; + var K = []; + + // Compute constants + (function () { + function isPrime(n) { + var sqrtN = Math.sqrt(n); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n % factor)) { + return false; + } + } + + return true; + } + + function getFractionalBits(n) { + return ((n - (n | 0)) * 0x100000000) | 0; + } + + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); + + nPrime++; + } + + n++; + } + }()); + + // Reusable object + var W = []; + + /** + * SHA-256 hash algorithm. + */ + var SHA256 = C_algo.SHA256 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init(H.slice(0)); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + var f = H[5]; + var g = H[6]; + var h = H[7]; + + // Computation + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ + ((gamma0x << 14) | (gamma0x >>> 18)) ^ + (gamma0x >>> 3); + + var gamma1x = W[i - 2]; + var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ + ((gamma1x << 13) | (gamma1x >>> 19)) ^ + (gamma1x >>> 10); + + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + + var ch = (e & f) ^ (~e & g); + var maj = (a & b) ^ (a & c) ^ (b & c); + + var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); + var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); + + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + + h = g; + g = f; + f = e; + e = (d + t1) | 0; + d = c; + c = b; + b = a; + a = (t1 + t2) | 0; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + H[5] = (H[5] + f) | 0; + H[6] = (H[6] + g) | 0; + H[7] = (H[7] + h) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA256('message'); + * var hash = CryptoJS.SHA256(wordArray); + */ + C.SHA256 = Hasher._createHelper(SHA256); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA256(message, key); + */ + C.HmacSHA256 = Hasher._createHmacHelper(SHA256); + }(Math)); + + + return CryptoJS.SHA256; + +})); diff --git a/module/static/aes/crypto-js-master/sha3.js b/module/static/aes/crypto-js-master/sha3.js new file mode 100644 index 0000000..6685057 --- /dev/null +++ b/module/static/aes/crypto-js-master/sha3.js @@ -0,0 +1,326 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./x64-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./x64-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var C_algo = C.algo; + + // Constants tables + var RHO_OFFSETS = []; + var PI_INDEXES = []; + var ROUND_CONSTANTS = []; + + // Compute Constants + (function () { + // Compute rho offset constants + var x = 1, y = 0; + for (var t = 0; t < 24; t++) { + RHO_OFFSETS[x + 5 * y] = ((t + 1) * (t + 2) / 2) % 64; + + var newX = y % 5; + var newY = (2 * x + 3 * y) % 5; + x = newX; + y = newY; + } + + // Compute pi index constants + for (var x = 0; x < 5; x++) { + for (var y = 0; y < 5; y++) { + PI_INDEXES[x + 5 * y] = y + ((2 * x + 3 * y) % 5) * 5; + } + } + + // Compute round constants + var LFSR = 0x01; + for (var i = 0; i < 24; i++) { + var roundConstantMsw = 0; + var roundConstantLsw = 0; + + for (var j = 0; j < 7; j++) { + if (LFSR & 0x01) { + var bitPosition = (1 << j) - 1; + if (bitPosition < 32) { + roundConstantLsw ^= 1 << bitPosition; + } else /* if (bitPosition >= 32) */ { + roundConstantMsw ^= 1 << (bitPosition - 32); + } + } + + // Compute next LFSR + if (LFSR & 0x80) { + // Primitive polynomial over GF(2): x^8 + x^6 + x^5 + x^4 + 1 + LFSR = (LFSR << 1) ^ 0x71; + } else { + LFSR <<= 1; + } + } + + ROUND_CONSTANTS[i] = X64Word.create(roundConstantMsw, roundConstantLsw); + } + }()); + + // Reusable objects for temporary values + var T = []; + (function () { + for (var i = 0; i < 25; i++) { + T[i] = X64Word.create(); + } + }()); + + /** + * SHA-3 hash algorithm. + */ + var SHA3 = C_algo.SHA3 = Hasher.extend({ + /** + * Configuration options. + * + * @property {number} outputLength + * The desired number of bits in the output hash. + * Only values permitted are: 224, 256, 384, 512. + * Default: 512 + */ + cfg: Hasher.cfg.extend({ + outputLength: 512 + }), + + _doReset: function () { + var state = this._state = [] + for (var i = 0; i < 25; i++) { + state[i] = new X64Word.init(); + } + + this.blockSize = (1600 - 2 * this.cfg.outputLength) / 32; + }, + + _doProcessBlock: function (M, offset) { + // Shortcuts + var state = this._state; + var nBlockSizeLanes = this.blockSize / 2; + + // Absorb + for (var i = 0; i < nBlockSizeLanes; i++) { + // Shortcuts + var M2i = M[offset + 2 * i]; + var M2i1 = M[offset + 2 * i + 1]; + + // Swap endian + M2i = ( + (((M2i << 8) | (M2i >>> 24)) & 0x00ff00ff) | + (((M2i << 24) | (M2i >>> 8)) & 0xff00ff00) + ); + M2i1 = ( + (((M2i1 << 8) | (M2i1 >>> 24)) & 0x00ff00ff) | + (((M2i1 << 24) | (M2i1 >>> 8)) & 0xff00ff00) + ); + + // Absorb message into state + var lane = state[i]; + lane.high ^= M2i1; + lane.low ^= M2i; + } + + // Rounds + for (var round = 0; round < 24; round++) { + // Theta + for (var x = 0; x < 5; x++) { + // Mix column lanes + var tMsw = 0, tLsw = 0; + for (var y = 0; y < 5; y++) { + var lane = state[x + 5 * y]; + tMsw ^= lane.high; + tLsw ^= lane.low; + } + + // Temporary values + var Tx = T[x]; + Tx.high = tMsw; + Tx.low = tLsw; + } + for (var x = 0; x < 5; x++) { + // Shortcuts + var Tx4 = T[(x + 4) % 5]; + var Tx1 = T[(x + 1) % 5]; + var Tx1Msw = Tx1.high; + var Tx1Lsw = Tx1.low; + + // Mix surrounding columns + var tMsw = Tx4.high ^ ((Tx1Msw << 1) | (Tx1Lsw >>> 31)); + var tLsw = Tx4.low ^ ((Tx1Lsw << 1) | (Tx1Msw >>> 31)); + for (var y = 0; y < 5; y++) { + var lane = state[x + 5 * y]; + lane.high ^= tMsw; + lane.low ^= tLsw; + } + } + + // Rho Pi + for (var laneIndex = 1; laneIndex < 25; laneIndex++) { + var tMsw; + var tLsw; + + // Shortcuts + var lane = state[laneIndex]; + var laneMsw = lane.high; + var laneLsw = lane.low; + var rhoOffset = RHO_OFFSETS[laneIndex]; + + // Rotate lanes + if (rhoOffset < 32) { + tMsw = (laneMsw << rhoOffset) | (laneLsw >>> (32 - rhoOffset)); + tLsw = (laneLsw << rhoOffset) | (laneMsw >>> (32 - rhoOffset)); + } else /* if (rhoOffset >= 32) */ { + tMsw = (laneLsw << (rhoOffset - 32)) | (laneMsw >>> (64 - rhoOffset)); + tLsw = (laneMsw << (rhoOffset - 32)) | (laneLsw >>> (64 - rhoOffset)); + } + + // Transpose lanes + var TPiLane = T[PI_INDEXES[laneIndex]]; + TPiLane.high = tMsw; + TPiLane.low = tLsw; + } + + // Rho pi at x = y = 0 + var T0 = T[0]; + var state0 = state[0]; + T0.high = state0.high; + T0.low = state0.low; + + // Chi + for (var x = 0; x < 5; x++) { + for (var y = 0; y < 5; y++) { + // Shortcuts + var laneIndex = x + 5 * y; + var lane = state[laneIndex]; + var TLane = T[laneIndex]; + var Tx1Lane = T[((x + 1) % 5) + 5 * y]; + var Tx2Lane = T[((x + 2) % 5) + 5 * y]; + + // Mix rows + lane.high = TLane.high ^ (~Tx1Lane.high & Tx2Lane.high); + lane.low = TLane.low ^ (~Tx1Lane.low & Tx2Lane.low); + } + } + + // Iota + var lane = state[0]; + var roundConstant = ROUND_CONSTANTS[round]; + lane.high ^= roundConstant.high; + lane.low ^= roundConstant.low; + } + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + var blockSizeBits = this.blockSize * 32; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x1 << (24 - nBitsLeft % 32); + dataWords[((Math.ceil((nBitsLeft + 1) / blockSizeBits) * blockSizeBits) >>> 5) - 1] |= 0x80; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var state = this._state; + var outputLengthBytes = this.cfg.outputLength / 8; + var outputLengthLanes = outputLengthBytes / 8; + + // Squeeze + var hashWords = []; + for (var i = 0; i < outputLengthLanes; i++) { + // Shortcuts + var lane = state[i]; + var laneMsw = lane.high; + var laneLsw = lane.low; + + // Swap endian + laneMsw = ( + (((laneMsw << 8) | (laneMsw >>> 24)) & 0x00ff00ff) | + (((laneMsw << 24) | (laneMsw >>> 8)) & 0xff00ff00) + ); + laneLsw = ( + (((laneLsw << 8) | (laneLsw >>> 24)) & 0x00ff00ff) | + (((laneLsw << 24) | (laneLsw >>> 8)) & 0xff00ff00) + ); + + // Squeeze state to retrieve hash + hashWords.push(laneLsw); + hashWords.push(laneMsw); + } + + // Return final computed hash + return new WordArray.init(hashWords, outputLengthBytes); + }, + + clone: function () { + var clone = Hasher.clone.call(this); + + var state = clone._state = this._state.slice(0); + for (var i = 0; i < 25; i++) { + state[i] = state[i].clone(); + } + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA3('message'); + * var hash = CryptoJS.SHA3(wordArray); + */ + C.SHA3 = Hasher._createHelper(SHA3); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA3(message, key); + */ + C.HmacSHA3 = Hasher._createHmacHelper(SHA3); + }(Math)); + + + return CryptoJS.SHA3; + +})); diff --git a/module/static/aes/crypto-js-master/sha384.js b/module/static/aes/crypto-js-master/sha384.js new file mode 100644 index 0000000..7783bf6 --- /dev/null +++ b/module/static/aes/crypto-js-master/sha384.js @@ -0,0 +1,83 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./x64-core"), require("./sha512")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./x64-core", "./sha512"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var X64WordArray = C_x64.WordArray; + var C_algo = C.algo; + var SHA512 = C_algo.SHA512; + + /** + * SHA-384 hash algorithm. + */ + var SHA384 = C_algo.SHA384 = SHA512.extend({ + _doReset: function () { + this._hash = new X64WordArray.init([ + new X64Word.init(0xcbbb9d5d, 0xc1059ed8), new X64Word.init(0x629a292a, 0x367cd507), + new X64Word.init(0x9159015a, 0x3070dd17), new X64Word.init(0x152fecd8, 0xf70e5939), + new X64Word.init(0x67332667, 0xffc00b31), new X64Word.init(0x8eb44a87, 0x68581511), + new X64Word.init(0xdb0c2e0d, 0x64f98fa7), new X64Word.init(0x47b5481d, 0xbefa4fa4) + ]); + }, + + _doFinalize: function () { + var hash = SHA512._doFinalize.call(this); + + hash.sigBytes -= 16; + + return hash; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA384('message'); + * var hash = CryptoJS.SHA384(wordArray); + */ + C.SHA384 = SHA512._createHelper(SHA384); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA384(message, key); + */ + C.HmacSHA384 = SHA512._createHmacHelper(SHA384); + }()); + + + return CryptoJS.SHA384; + +})); diff --git a/module/static/aes/crypto-js-master/sha512.js b/module/static/aes/crypto-js-master/sha512.js new file mode 100644 index 0000000..85ccc9e --- /dev/null +++ b/module/static/aes/crypto-js-master/sha512.js @@ -0,0 +1,326 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./x64-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./x64-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Hasher = C_lib.Hasher; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var X64WordArray = C_x64.WordArray; + var C_algo = C.algo; + + function X64Word_create() { + return X64Word.create.apply(X64Word, arguments); + } + + // Constants + var K = [ + X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd), + X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc), + X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019), + X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118), + X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe), + X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2), + X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1), + X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694), + X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3), + X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65), + X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483), + X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5), + X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210), + X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4), + X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725), + X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70), + X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926), + X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df), + X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8), + X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b), + X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001), + X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30), + X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910), + X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8), + X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53), + X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8), + X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb), + X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3), + X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60), + X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec), + X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9), + X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b), + X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207), + X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178), + X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6), + X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b), + X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493), + X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c), + X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a), + X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817) + ]; + + // Reusable objects + var W = []; + (function () { + for (var i = 0; i < 80; i++) { + W[i] = X64Word_create(); + } + }()); + + /** + * SHA-512 hash algorithm. + */ + var SHA512 = C_algo.SHA512 = Hasher.extend({ + _doReset: function () { + this._hash = new X64WordArray.init([ + new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b), + new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1), + new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f), + new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179) + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcuts + var H = this._hash.words; + + var H0 = H[0]; + var H1 = H[1]; + var H2 = H[2]; + var H3 = H[3]; + var H4 = H[4]; + var H5 = H[5]; + var H6 = H[6]; + var H7 = H[7]; + + var H0h = H0.high; + var H0l = H0.low; + var H1h = H1.high; + var H1l = H1.low; + var H2h = H2.high; + var H2l = H2.low; + var H3h = H3.high; + var H3l = H3.low; + var H4h = H4.high; + var H4l = H4.low; + var H5h = H5.high; + var H5l = H5.low; + var H6h = H6.high; + var H6l = H6.low; + var H7h = H7.high; + var H7l = H7.low; + + // Working variables + var ah = H0h; + var al = H0l; + var bh = H1h; + var bl = H1l; + var ch = H2h; + var cl = H2l; + var dh = H3h; + var dl = H3l; + var eh = H4h; + var el = H4l; + var fh = H5h; + var fl = H5l; + var gh = H6h; + var gl = H6l; + var hh = H7h; + var hl = H7l; + + // Rounds + for (var i = 0; i < 80; i++) { + var Wil; + var Wih; + + // Shortcut + var Wi = W[i]; + + // Extend message + if (i < 16) { + Wih = Wi.high = M[offset + i * 2] | 0; + Wil = Wi.low = M[offset + i * 2 + 1] | 0; + } else { + // Gamma0 + var gamma0x = W[i - 15]; + var gamma0xh = gamma0x.high; + var gamma0xl = gamma0x.low; + var gamma0h = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7); + var gamma0l = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25)); + + // Gamma1 + var gamma1x = W[i - 2]; + var gamma1xh = gamma1x.high; + var gamma1xl = gamma1x.low; + var gamma1h = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6); + var gamma1l = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26)); + + // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16] + var Wi7 = W[i - 7]; + var Wi7h = Wi7.high; + var Wi7l = Wi7.low; + + var Wi16 = W[i - 16]; + var Wi16h = Wi16.high; + var Wi16l = Wi16.low; + + Wil = gamma0l + Wi7l; + Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0); + Wil = Wil + gamma1l; + Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0); + Wil = Wil + Wi16l; + Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0); + + Wi.high = Wih; + Wi.low = Wil; + } + + var chh = (eh & fh) ^ (~eh & gh); + var chl = (el & fl) ^ (~el & gl); + var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch); + var majl = (al & bl) ^ (al & cl) ^ (bl & cl); + + var sigma0h = ((ah >>> 28) | (al << 4)) ^ ((ah << 30) | (al >>> 2)) ^ ((ah << 25) | (al >>> 7)); + var sigma0l = ((al >>> 28) | (ah << 4)) ^ ((al << 30) | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7)); + var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9)); + var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9)); + + // t1 = h + sigma1 + ch + K[i] + W[i] + var Ki = K[i]; + var Kih = Ki.high; + var Kil = Ki.low; + + var t1l = hl + sigma1l; + var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0); + var t1l = t1l + chl; + var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0); + var t1l = t1l + Kil; + var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0); + var t1l = t1l + Wil; + var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0); + + // t2 = sigma0 + maj + var t2l = sigma0l + majl; + var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0); + + // Update working variables + hh = gh; + hl = gl; + gh = fh; + gl = fl; + fh = eh; + fl = el; + el = (dl + t1l) | 0; + eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0; + dh = ch; + dl = cl; + ch = bh; + cl = bl; + bh = ah; + bl = al; + al = (t1l + t2l) | 0; + ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0; + } + + // Intermediate hash value + H0l = H0.low = (H0l + al); + H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0)); + H1l = H1.low = (H1l + bl); + H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0)); + H2l = H2.low = (H2l + cl); + H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0)); + H3l = H3.low = (H3l + dl); + H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0)); + H4l = H4.low = (H4l + el); + H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0)); + H5l = H5.low = (H5l + fl); + H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0)); + H6l = H6.low = (H6l + gl); + H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0)); + H7l = H7.low = (H7l + hl); + H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0)); + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Convert hash to 32-bit word array before returning + var hash = this._hash.toX32(); + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + }, + + blockSize: 1024/32 + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA512('message'); + * var hash = CryptoJS.SHA512(wordArray); + */ + C.SHA512 = Hasher._createHelper(SHA512); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA512(message, key); + */ + C.HmacSHA512 = Hasher._createHmacHelper(SHA512); + }()); + + + return CryptoJS.SHA512; + +})); diff --git a/module/static/aes/crypto-js-master/tripledes.js b/module/static/aes/crypto-js-master/tripledes.js new file mode 100644 index 0000000..f05d703 --- /dev/null +++ b/module/static/aes/crypto-js-master/tripledes.js @@ -0,0 +1,779 @@ +;(function (root, factory, undef) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core"), require("./enc-base64"), require("./md5"), require("./evpkdf"), require("./cipher-core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core", "./enc-base64", "./md5", "./evpkdf", "./cipher-core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var BlockCipher = C_lib.BlockCipher; + var C_algo = C.algo; + + // Permuted Choice 1 constants + var PC1 = [ + 57, 49, 41, 33, 25, 17, 9, 1, + 58, 50, 42, 34, 26, 18, 10, 2, + 59, 51, 43, 35, 27, 19, 11, 3, + 60, 52, 44, 36, 63, 55, 47, 39, + 31, 23, 15, 7, 62, 54, 46, 38, + 30, 22, 14, 6, 61, 53, 45, 37, + 29, 21, 13, 5, 28, 20, 12, 4 + ]; + + // Permuted Choice 2 constants + var PC2 = [ + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32 + ]; + + // Cumulative bit shift constants + var BIT_SHIFTS = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28]; + + // SBOXes and round permutation constants + var SBOX_P = [ + { + 0x0: 0x808200, + 0x10000000: 0x8000, + 0x20000000: 0x808002, + 0x30000000: 0x2, + 0x40000000: 0x200, + 0x50000000: 0x808202, + 0x60000000: 0x800202, + 0x70000000: 0x800000, + 0x80000000: 0x202, + 0x90000000: 0x800200, + 0xa0000000: 0x8200, + 0xb0000000: 0x808000, + 0xc0000000: 0x8002, + 0xd0000000: 0x800002, + 0xe0000000: 0x0, + 0xf0000000: 0x8202, + 0x8000000: 0x0, + 0x18000000: 0x808202, + 0x28000000: 0x8202, + 0x38000000: 0x8000, + 0x48000000: 0x808200, + 0x58000000: 0x200, + 0x68000000: 0x808002, + 0x78000000: 0x2, + 0x88000000: 0x800200, + 0x98000000: 0x8200, + 0xa8000000: 0x808000, + 0xb8000000: 0x800202, + 0xc8000000: 0x800002, + 0xd8000000: 0x8002, + 0xe8000000: 0x202, + 0xf8000000: 0x800000, + 0x1: 0x8000, + 0x10000001: 0x2, + 0x20000001: 0x808200, + 0x30000001: 0x800000, + 0x40000001: 0x808002, + 0x50000001: 0x8200, + 0x60000001: 0x200, + 0x70000001: 0x800202, + 0x80000001: 0x808202, + 0x90000001: 0x808000, + 0xa0000001: 0x800002, + 0xb0000001: 0x8202, + 0xc0000001: 0x202, + 0xd0000001: 0x800200, + 0xe0000001: 0x8002, + 0xf0000001: 0x0, + 0x8000001: 0x808202, + 0x18000001: 0x808000, + 0x28000001: 0x800000, + 0x38000001: 0x200, + 0x48000001: 0x8000, + 0x58000001: 0x800002, + 0x68000001: 0x2, + 0x78000001: 0x8202, + 0x88000001: 0x8002, + 0x98000001: 0x800202, + 0xa8000001: 0x202, + 0xb8000001: 0x808200, + 0xc8000001: 0x800200, + 0xd8000001: 0x0, + 0xe8000001: 0x8200, + 0xf8000001: 0x808002 + }, + { + 0x0: 0x40084010, + 0x1000000: 0x4000, + 0x2000000: 0x80000, + 0x3000000: 0x40080010, + 0x4000000: 0x40000010, + 0x5000000: 0x40084000, + 0x6000000: 0x40004000, + 0x7000000: 0x10, + 0x8000000: 0x84000, + 0x9000000: 0x40004010, + 0xa000000: 0x40000000, + 0xb000000: 0x84010, + 0xc000000: 0x80010, + 0xd000000: 0x0, + 0xe000000: 0x4010, + 0xf000000: 0x40080000, + 0x800000: 0x40004000, + 0x1800000: 0x84010, + 0x2800000: 0x10, + 0x3800000: 0x40004010, + 0x4800000: 0x40084010, + 0x5800000: 0x40000000, + 0x6800000: 0x80000, + 0x7800000: 0x40080010, + 0x8800000: 0x80010, + 0x9800000: 0x0, + 0xa800000: 0x4000, + 0xb800000: 0x40080000, + 0xc800000: 0x40000010, + 0xd800000: 0x84000, + 0xe800000: 0x40084000, + 0xf800000: 0x4010, + 0x10000000: 0x0, + 0x11000000: 0x40080010, + 0x12000000: 0x40004010, + 0x13000000: 0x40084000, + 0x14000000: 0x40080000, + 0x15000000: 0x10, + 0x16000000: 0x84010, + 0x17000000: 0x4000, + 0x18000000: 0x4010, + 0x19000000: 0x80000, + 0x1a000000: 0x80010, + 0x1b000000: 0x40000010, + 0x1c000000: 0x84000, + 0x1d000000: 0x40004000, + 0x1e000000: 0x40000000, + 0x1f000000: 0x40084010, + 0x10800000: 0x84010, + 0x11800000: 0x80000, + 0x12800000: 0x40080000, + 0x13800000: 0x4000, + 0x14800000: 0x40004000, + 0x15800000: 0x40084010, + 0x16800000: 0x10, + 0x17800000: 0x40000000, + 0x18800000: 0x40084000, + 0x19800000: 0x40000010, + 0x1a800000: 0x40004010, + 0x1b800000: 0x80010, + 0x1c800000: 0x0, + 0x1d800000: 0x4010, + 0x1e800000: 0x40080010, + 0x1f800000: 0x84000 + }, + { + 0x0: 0x104, + 0x100000: 0x0, + 0x200000: 0x4000100, + 0x300000: 0x10104, + 0x400000: 0x10004, + 0x500000: 0x4000004, + 0x600000: 0x4010104, + 0x700000: 0x4010000, + 0x800000: 0x4000000, + 0x900000: 0x4010100, + 0xa00000: 0x10100, + 0xb00000: 0x4010004, + 0xc00000: 0x4000104, + 0xd00000: 0x10000, + 0xe00000: 0x4, + 0xf00000: 0x100, + 0x80000: 0x4010100, + 0x180000: 0x4010004, + 0x280000: 0x0, + 0x380000: 0x4000100, + 0x480000: 0x4000004, + 0x580000: 0x10000, + 0x680000: 0x10004, + 0x780000: 0x104, + 0x880000: 0x4, + 0x980000: 0x100, + 0xa80000: 0x4010000, + 0xb80000: 0x10104, + 0xc80000: 0x10100, + 0xd80000: 0x4000104, + 0xe80000: 0x4010104, + 0xf80000: 0x4000000, + 0x1000000: 0x4010100, + 0x1100000: 0x10004, + 0x1200000: 0x10000, + 0x1300000: 0x4000100, + 0x1400000: 0x100, + 0x1500000: 0x4010104, + 0x1600000: 0x4000004, + 0x1700000: 0x0, + 0x1800000: 0x4000104, + 0x1900000: 0x4000000, + 0x1a00000: 0x4, + 0x1b00000: 0x10100, + 0x1c00000: 0x4010000, + 0x1d00000: 0x104, + 0x1e00000: 0x10104, + 0x1f00000: 0x4010004, + 0x1080000: 0x4000000, + 0x1180000: 0x104, + 0x1280000: 0x4010100, + 0x1380000: 0x0, + 0x1480000: 0x10004, + 0x1580000: 0x4000100, + 0x1680000: 0x100, + 0x1780000: 0x4010004, + 0x1880000: 0x10000, + 0x1980000: 0x4010104, + 0x1a80000: 0x10104, + 0x1b80000: 0x4000004, + 0x1c80000: 0x4000104, + 0x1d80000: 0x4010000, + 0x1e80000: 0x4, + 0x1f80000: 0x10100 + }, + { + 0x0: 0x80401000, + 0x10000: 0x80001040, + 0x20000: 0x401040, + 0x30000: 0x80400000, + 0x40000: 0x0, + 0x50000: 0x401000, + 0x60000: 0x80000040, + 0x70000: 0x400040, + 0x80000: 0x80000000, + 0x90000: 0x400000, + 0xa0000: 0x40, + 0xb0000: 0x80001000, + 0xc0000: 0x80400040, + 0xd0000: 0x1040, + 0xe0000: 0x1000, + 0xf0000: 0x80401040, + 0x8000: 0x80001040, + 0x18000: 0x40, + 0x28000: 0x80400040, + 0x38000: 0x80001000, + 0x48000: 0x401000, + 0x58000: 0x80401040, + 0x68000: 0x0, + 0x78000: 0x80400000, + 0x88000: 0x1000, + 0x98000: 0x80401000, + 0xa8000: 0x400000, + 0xb8000: 0x1040, + 0xc8000: 0x80000000, + 0xd8000: 0x400040, + 0xe8000: 0x401040, + 0xf8000: 0x80000040, + 0x100000: 0x400040, + 0x110000: 0x401000, + 0x120000: 0x80000040, + 0x130000: 0x0, + 0x140000: 0x1040, + 0x150000: 0x80400040, + 0x160000: 0x80401000, + 0x170000: 0x80001040, + 0x180000: 0x80401040, + 0x190000: 0x80000000, + 0x1a0000: 0x80400000, + 0x1b0000: 0x401040, + 0x1c0000: 0x80001000, + 0x1d0000: 0x400000, + 0x1e0000: 0x40, + 0x1f0000: 0x1000, + 0x108000: 0x80400000, + 0x118000: 0x80401040, + 0x128000: 0x0, + 0x138000: 0x401000, + 0x148000: 0x400040, + 0x158000: 0x80000000, + 0x168000: 0x80001040, + 0x178000: 0x40, + 0x188000: 0x80000040, + 0x198000: 0x1000, + 0x1a8000: 0x80001000, + 0x1b8000: 0x80400040, + 0x1c8000: 0x1040, + 0x1d8000: 0x80401000, + 0x1e8000: 0x400000, + 0x1f8000: 0x401040 + }, + { + 0x0: 0x80, + 0x1000: 0x1040000, + 0x2000: 0x40000, + 0x3000: 0x20000000, + 0x4000: 0x20040080, + 0x5000: 0x1000080, + 0x6000: 0x21000080, + 0x7000: 0x40080, + 0x8000: 0x1000000, + 0x9000: 0x20040000, + 0xa000: 0x20000080, + 0xb000: 0x21040080, + 0xc000: 0x21040000, + 0xd000: 0x0, + 0xe000: 0x1040080, + 0xf000: 0x21000000, + 0x800: 0x1040080, + 0x1800: 0x21000080, + 0x2800: 0x80, + 0x3800: 0x1040000, + 0x4800: 0x40000, + 0x5800: 0x20040080, + 0x6800: 0x21040000, + 0x7800: 0x20000000, + 0x8800: 0x20040000, + 0x9800: 0x0, + 0xa800: 0x21040080, + 0xb800: 0x1000080, + 0xc800: 0x20000080, + 0xd800: 0x21000000, + 0xe800: 0x1000000, + 0xf800: 0x40080, + 0x10000: 0x40000, + 0x11000: 0x80, + 0x12000: 0x20000000, + 0x13000: 0x21000080, + 0x14000: 0x1000080, + 0x15000: 0x21040000, + 0x16000: 0x20040080, + 0x17000: 0x1000000, + 0x18000: 0x21040080, + 0x19000: 0x21000000, + 0x1a000: 0x1040000, + 0x1b000: 0x20040000, + 0x1c000: 0x40080, + 0x1d000: 0x20000080, + 0x1e000: 0x0, + 0x1f000: 0x1040080, + 0x10800: 0x21000080, + 0x11800: 0x1000000, + 0x12800: 0x1040000, + 0x13800: 0x20040080, + 0x14800: 0x20000000, + 0x15800: 0x1040080, + 0x16800: 0x80, + 0x17800: 0x21040000, + 0x18800: 0x40080, + 0x19800: 0x21040080, + 0x1a800: 0x0, + 0x1b800: 0x21000000, + 0x1c800: 0x1000080, + 0x1d800: 0x40000, + 0x1e800: 0x20040000, + 0x1f800: 0x20000080 + }, + { + 0x0: 0x10000008, + 0x100: 0x2000, + 0x200: 0x10200000, + 0x300: 0x10202008, + 0x400: 0x10002000, + 0x500: 0x200000, + 0x600: 0x200008, + 0x700: 0x10000000, + 0x800: 0x0, + 0x900: 0x10002008, + 0xa00: 0x202000, + 0xb00: 0x8, + 0xc00: 0x10200008, + 0xd00: 0x202008, + 0xe00: 0x2008, + 0xf00: 0x10202000, + 0x80: 0x10200000, + 0x180: 0x10202008, + 0x280: 0x8, + 0x380: 0x200000, + 0x480: 0x202008, + 0x580: 0x10000008, + 0x680: 0x10002000, + 0x780: 0x2008, + 0x880: 0x200008, + 0x980: 0x2000, + 0xa80: 0x10002008, + 0xb80: 0x10200008, + 0xc80: 0x0, + 0xd80: 0x10202000, + 0xe80: 0x202000, + 0xf80: 0x10000000, + 0x1000: 0x10002000, + 0x1100: 0x10200008, + 0x1200: 0x10202008, + 0x1300: 0x2008, + 0x1400: 0x200000, + 0x1500: 0x10000000, + 0x1600: 0x10000008, + 0x1700: 0x202000, + 0x1800: 0x202008, + 0x1900: 0x0, + 0x1a00: 0x8, + 0x1b00: 0x10200000, + 0x1c00: 0x2000, + 0x1d00: 0x10002008, + 0x1e00: 0x10202000, + 0x1f00: 0x200008, + 0x1080: 0x8, + 0x1180: 0x202000, + 0x1280: 0x200000, + 0x1380: 0x10000008, + 0x1480: 0x10002000, + 0x1580: 0x2008, + 0x1680: 0x10202008, + 0x1780: 0x10200000, + 0x1880: 0x10202000, + 0x1980: 0x10200008, + 0x1a80: 0x2000, + 0x1b80: 0x202008, + 0x1c80: 0x200008, + 0x1d80: 0x0, + 0x1e80: 0x10000000, + 0x1f80: 0x10002008 + }, + { + 0x0: 0x100000, + 0x10: 0x2000401, + 0x20: 0x400, + 0x30: 0x100401, + 0x40: 0x2100401, + 0x50: 0x0, + 0x60: 0x1, + 0x70: 0x2100001, + 0x80: 0x2000400, + 0x90: 0x100001, + 0xa0: 0x2000001, + 0xb0: 0x2100400, + 0xc0: 0x2100000, + 0xd0: 0x401, + 0xe0: 0x100400, + 0xf0: 0x2000000, + 0x8: 0x2100001, + 0x18: 0x0, + 0x28: 0x2000401, + 0x38: 0x2100400, + 0x48: 0x100000, + 0x58: 0x2000001, + 0x68: 0x2000000, + 0x78: 0x401, + 0x88: 0x100401, + 0x98: 0x2000400, + 0xa8: 0x2100000, + 0xb8: 0x100001, + 0xc8: 0x400, + 0xd8: 0x2100401, + 0xe8: 0x1, + 0xf8: 0x100400, + 0x100: 0x2000000, + 0x110: 0x100000, + 0x120: 0x2000401, + 0x130: 0x2100001, + 0x140: 0x100001, + 0x150: 0x2000400, + 0x160: 0x2100400, + 0x170: 0x100401, + 0x180: 0x401, + 0x190: 0x2100401, + 0x1a0: 0x100400, + 0x1b0: 0x1, + 0x1c0: 0x0, + 0x1d0: 0x2100000, + 0x1e0: 0x2000001, + 0x1f0: 0x400, + 0x108: 0x100400, + 0x118: 0x2000401, + 0x128: 0x2100001, + 0x138: 0x1, + 0x148: 0x2000000, + 0x158: 0x100000, + 0x168: 0x401, + 0x178: 0x2100400, + 0x188: 0x2000001, + 0x198: 0x2100000, + 0x1a8: 0x0, + 0x1b8: 0x2100401, + 0x1c8: 0x100401, + 0x1d8: 0x400, + 0x1e8: 0x2000400, + 0x1f8: 0x100001 + }, + { + 0x0: 0x8000820, + 0x1: 0x20000, + 0x2: 0x8000000, + 0x3: 0x20, + 0x4: 0x20020, + 0x5: 0x8020820, + 0x6: 0x8020800, + 0x7: 0x800, + 0x8: 0x8020000, + 0x9: 0x8000800, + 0xa: 0x20800, + 0xb: 0x8020020, + 0xc: 0x820, + 0xd: 0x0, + 0xe: 0x8000020, + 0xf: 0x20820, + 0x80000000: 0x800, + 0x80000001: 0x8020820, + 0x80000002: 0x8000820, + 0x80000003: 0x8000000, + 0x80000004: 0x8020000, + 0x80000005: 0x20800, + 0x80000006: 0x20820, + 0x80000007: 0x20, + 0x80000008: 0x8000020, + 0x80000009: 0x820, + 0x8000000a: 0x20020, + 0x8000000b: 0x8020800, + 0x8000000c: 0x0, + 0x8000000d: 0x8020020, + 0x8000000e: 0x8000800, + 0x8000000f: 0x20000, + 0x10: 0x20820, + 0x11: 0x8020800, + 0x12: 0x20, + 0x13: 0x800, + 0x14: 0x8000800, + 0x15: 0x8000020, + 0x16: 0x8020020, + 0x17: 0x20000, + 0x18: 0x0, + 0x19: 0x20020, + 0x1a: 0x8020000, + 0x1b: 0x8000820, + 0x1c: 0x8020820, + 0x1d: 0x20800, + 0x1e: 0x820, + 0x1f: 0x8000000, + 0x80000010: 0x20000, + 0x80000011: 0x800, + 0x80000012: 0x8020020, + 0x80000013: 0x20820, + 0x80000014: 0x20, + 0x80000015: 0x8020000, + 0x80000016: 0x8000000, + 0x80000017: 0x8000820, + 0x80000018: 0x8020820, + 0x80000019: 0x8000020, + 0x8000001a: 0x8000800, + 0x8000001b: 0x0, + 0x8000001c: 0x20800, + 0x8000001d: 0x820, + 0x8000001e: 0x20020, + 0x8000001f: 0x8020800 + } + ]; + + // Masks that select the SBOX input + var SBOX_MASK = [ + 0xf8000001, 0x1f800000, 0x01f80000, 0x001f8000, + 0x0001f800, 0x00001f80, 0x000001f8, 0x8000001f + ]; + + /** + * DES block cipher algorithm. + */ + var DES = C_algo.DES = BlockCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + + // Select 56 bits according to PC1 + var keyBits = []; + for (var i = 0; i < 56; i++) { + var keyBitPos = PC1[i] - 1; + keyBits[i] = (keyWords[keyBitPos >>> 5] >>> (31 - keyBitPos % 32)) & 1; + } + + // Assemble 16 subkeys + var subKeys = this._subKeys = []; + for (var nSubKey = 0; nSubKey < 16; nSubKey++) { + // Create subkey + var subKey = subKeys[nSubKey] = []; + + // Shortcut + var bitShift = BIT_SHIFTS[nSubKey]; + + // Select 48 bits according to PC2 + for (var i = 0; i < 24; i++) { + // Select from the left 28 key bits + subKey[(i / 6) | 0] |= keyBits[((PC2[i] - 1) + bitShift) % 28] << (31 - i % 6); + + // Select from the right 28 key bits + subKey[4 + ((i / 6) | 0)] |= keyBits[28 + (((PC2[i + 24] - 1) + bitShift) % 28)] << (31 - i % 6); + } + + // Since each subkey is applied to an expanded 32-bit input, + // the subkey can be broken into 8 values scaled to 32-bits, + // which allows the key to be used without expansion + subKey[0] = (subKey[0] << 1) | (subKey[0] >>> 31); + for (var i = 1; i < 7; i++) { + subKey[i] = subKey[i] >>> ((i - 1) * 4 + 3); + } + subKey[7] = (subKey[7] << 5) | (subKey[7] >>> 27); + } + + // Compute inverse subkeys + var invSubKeys = this._invSubKeys = []; + for (var i = 0; i < 16; i++) { + invSubKeys[i] = subKeys[15 - i]; + } + }, + + encryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._subKeys); + }, + + decryptBlock: function (M, offset) { + this._doCryptBlock(M, offset, this._invSubKeys); + }, + + _doCryptBlock: function (M, offset, subKeys) { + // Get input + this._lBlock = M[offset]; + this._rBlock = M[offset + 1]; + + // Initial permutation + exchangeLR.call(this, 4, 0x0f0f0f0f); + exchangeLR.call(this, 16, 0x0000ffff); + exchangeRL.call(this, 2, 0x33333333); + exchangeRL.call(this, 8, 0x00ff00ff); + exchangeLR.call(this, 1, 0x55555555); + + // Rounds + for (var round = 0; round < 16; round++) { + // Shortcuts + var subKey = subKeys[round]; + var lBlock = this._lBlock; + var rBlock = this._rBlock; + + // Feistel function + var f = 0; + for (var i = 0; i < 8; i++) { + f |= SBOX_P[i][((rBlock ^ subKey[i]) & SBOX_MASK[i]) >>> 0]; + } + this._lBlock = rBlock; + this._rBlock = lBlock ^ f; + } + + // Undo swap from last round + var t = this._lBlock; + this._lBlock = this._rBlock; + this._rBlock = t; + + // Final permutation + exchangeLR.call(this, 1, 0x55555555); + exchangeRL.call(this, 8, 0x00ff00ff); + exchangeRL.call(this, 2, 0x33333333); + exchangeLR.call(this, 16, 0x0000ffff); + exchangeLR.call(this, 4, 0x0f0f0f0f); + + // Set output + M[offset] = this._lBlock; + M[offset + 1] = this._rBlock; + }, + + keySize: 64/32, + + ivSize: 64/32, + + blockSize: 64/32 + }); + + // Swap bits across the left and right words + function exchangeLR(offset, mask) { + var t = ((this._lBlock >>> offset) ^ this._rBlock) & mask; + this._rBlock ^= t; + this._lBlock ^= t << offset; + } + + function exchangeRL(offset, mask) { + var t = ((this._rBlock >>> offset) ^ this._lBlock) & mask; + this._lBlock ^= t; + this._rBlock ^= t << offset; + } + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.DES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.DES.decrypt(ciphertext, key, cfg); + */ + C.DES = BlockCipher._createHelper(DES); + + /** + * Triple-DES block cipher algorithm. + */ + var TripleDES = C_algo.TripleDES = BlockCipher.extend({ + _doReset: function () { + // Shortcuts + var key = this._key; + var keyWords = key.words; + // Make sure the key length is valid (64, 128 or >= 192 bit) + if (keyWords.length !== 2 && keyWords.length !== 4 && keyWords.length < 6) { + throw new Error('Invalid key length - 3DES requires the key length to be 64, 128, 192 or >192.'); + } + + // Extend the key according to the keying options defined in 3DES standard + var key1 = keyWords.slice(0, 2); + var key2 = keyWords.length < 4 ? keyWords.slice(0, 2) : keyWords.slice(2, 4); + var key3 = keyWords.length < 6 ? keyWords.slice(0, 2) : keyWords.slice(4, 6); + + // Create DES instances + this._des1 = DES.createEncryptor(WordArray.create(key1)); + this._des2 = DES.createEncryptor(WordArray.create(key2)); + this._des3 = DES.createEncryptor(WordArray.create(key3)); + }, + + encryptBlock: function (M, offset) { + this._des1.encryptBlock(M, offset); + this._des2.decryptBlock(M, offset); + this._des3.encryptBlock(M, offset); + }, + + decryptBlock: function (M, offset) { + this._des3.decryptBlock(M, offset); + this._des2.encryptBlock(M, offset); + this._des1.decryptBlock(M, offset); + }, + + keySize: 192/32, + + ivSize: 64/32, + + blockSize: 64/32 + }); + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.TripleDES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.TripleDES.decrypt(ciphertext, key, cfg); + */ + C.TripleDES = BlockCipher._createHelper(TripleDES); + }()); + + + return CryptoJS.TripleDES; + +})); diff --git a/module/static/aes/crypto-js-master/x64-core.js b/module/static/aes/crypto-js-master/x64-core.js new file mode 100644 index 0000000..5e401f2 --- /dev/null +++ b/module/static/aes/crypto-js-master/x64-core.js @@ -0,0 +1,304 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var X32WordArray = C_lib.WordArray; + + /** + * x64 namespace. + */ + var C_x64 = C.x64 = {}; + + /** + * A 64-bit word. + */ + var X64Word = C_x64.Word = Base.extend({ + /** + * Initializes a newly created 64-bit word. + * + * @param {number} high The high 32 bits. + * @param {number} low The low 32 bits. + * + * @example + * + * var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607); + */ + init: function (high, low) { + this.high = high; + this.low = low; + } + + /** + * Bitwise NOTs this word. + * + * @return {X64Word} A new x64-Word object after negating. + * + * @example + * + * var negated = x64Word.not(); + */ + // not: function () { + // var high = ~this.high; + // var low = ~this.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ANDs this word with the passed word. + * + * @param {X64Word} word The x64-Word to AND with this word. + * + * @return {X64Word} A new x64-Word object after ANDing. + * + * @example + * + * var anded = x64Word.and(anotherX64Word); + */ + // and: function (word) { + // var high = this.high & word.high; + // var low = this.low & word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to OR with this word. + * + * @return {X64Word} A new x64-Word object after ORing. + * + * @example + * + * var ored = x64Word.or(anotherX64Word); + */ + // or: function (word) { + // var high = this.high | word.high; + // var low = this.low | word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise XORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to XOR with this word. + * + * @return {X64Word} A new x64-Word object after XORing. + * + * @example + * + * var xored = x64Word.xor(anotherX64Word); + */ + // xor: function (word) { + // var high = this.high ^ word.high; + // var low = this.low ^ word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the left. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftL(25); + */ + // shiftL: function (n) { + // if (n < 32) { + // var high = (this.high << n) | (this.low >>> (32 - n)); + // var low = this.low << n; + // } else { + // var high = this.low << (n - 32); + // var low = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the right. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftR(7); + */ + // shiftR: function (n) { + // if (n < 32) { + // var low = (this.low >>> n) | (this.high << (32 - n)); + // var high = this.high >>> n; + // } else { + // var low = this.high >>> (n - 32); + // var high = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Rotates this word n bits to the left. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotL(25); + */ + // rotL: function (n) { + // return this.shiftL(n).or(this.shiftR(64 - n)); + // }, + + /** + * Rotates this word n bits to the right. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotR(7); + */ + // rotR: function (n) { + // return this.shiftR(n).or(this.shiftL(64 - n)); + // }, + + /** + * Adds this word with the passed word. + * + * @param {X64Word} word The x64-Word to add with this word. + * + * @return {X64Word} A new x64-Word object after adding. + * + * @example + * + * var added = x64Word.add(anotherX64Word); + */ + // add: function (word) { + // var low = (this.low + word.low) | 0; + // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0; + // var high = (this.high + word.high + carry) | 0; + + // return X64Word.create(high, low); + // } + }); + + /** + * An array of 64-bit words. + * + * @property {Array} words The array of CryptoJS.x64.Word objects. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var X64WordArray = C_x64.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.x64.WordArray.create(); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ]); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ], 10); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 8; + } + }, + + /** + * Converts this 64-bit word array to a 32-bit word array. + * + * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array. + * + * @example + * + * var x32WordArray = x64WordArray.toX32(); + */ + toX32: function () { + // Shortcuts + var x64Words = this.words; + var x64WordsLength = x64Words.length; + + // Convert + var x32Words = []; + for (var i = 0; i < x64WordsLength; i++) { + var x64Word = x64Words[i]; + x32Words.push(x64Word.high); + x32Words.push(x64Word.low); + } + + return X32WordArray.create(x32Words, this.sigBytes); + }, + + /** + * Creates a copy of this word array. + * + * @return {X64WordArray} The clone. + * + * @example + * + * var clone = x64WordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + + // Clone "words" array + var words = clone.words = this.words.slice(0); + + // Clone each X64Word object + var wordsLength = words.length; + for (var i = 0; i < wordsLength; i++) { + words[i] = words[i].clone(); + } + + return clone; + } + }); + }()); + + + return CryptoJS; + +})); diff --git a/module/static/css/index.css b/module/static/css/index.css new file mode 100644 index 0000000..7407fee --- /dev/null +++ b/module/static/css/index.css @@ -0,0 +1,16 @@ +.header-title { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} +.header-title .stop-btn { + padding: 8px; + border: 1px solid #009688; + border-radius: 4px; + font-size: 16px; + color: #009688; + cursor: pointer; +} + +/*# sourceMappingURL=index.css.map */ diff --git a/module/static/layui/css/layui.css b/module/static/layui/css/layui.css new file mode 100644 index 0000000..f8fdf94 --- /dev/null +++ b/module/static/layui/css/layui.css @@ -0,0 +1 @@ +blockquote,body,button,dd,div,dl,dt,form,h1,h2,h3,h4,h5,h6,input,li,ol,p,pre,td,textarea,th,ul{margin:0;padding:0;-webkit-tap-highlight-color:rgba(0,0,0,0)}a:active,a:hover{outline:0}img{display:inline-block;border:none;vertical-align:middle}li{list-style:none}table{border-collapse:collapse;border-spacing:0}h1,h2,h3{font-weight:400}h4,h5,h6{font-size:100%;font-weight:400}button,input,select,textarea{font-size:100%}button,input,optgroup,option,select,textarea{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;outline:0}pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}body{line-height:1.6;color:#333;color:rgba(0,0,0,.85);font:14px Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif}hr{height:0;line-height:0;margin:10px 0;padding:0;border:none!important;border-bottom:1px solid #eee!important;clear:both;overflow:hidden;background:0 0}a{color:#333;text-decoration:none}a:hover{color:#777}a cite{font-style:normal;*cursor:pointer}.layui-border-box,.layui-border-box *{box-sizing:border-box}.layui-box,.layui-box *{box-sizing:content-box}.layui-clear{clear:both;*zoom:1}.layui-clear:after{content:'\20';clear:both;*zoom:1;display:block;height:0}.layui-inline{position:relative;display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.layui-edge{position:relative;display:inline-block;vertical-align:middle;width:0;height:0;border-width:6px;border-style:dashed;border-color:transparent;overflow:hidden}.layui-edge-top{top:-4px;border-bottom-color:#999;border-bottom-style:solid}.layui-edge-right{border-left-color:#999;border-left-style:solid}.layui-edge-bottom{top:2px;border-top-color:#999;border-top-style:solid}.layui-edge-left{border-right-color:#999;border-right-style:solid}.layui-elip{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-disabled,.layui-icon,.layui-unselect{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-disabled,.layui-disabled:hover{color:#d2d2d2!important;cursor:not-allowed!important}.layui-circle{border-radius:100%}.layui-show{display:block!important}.layui-hide{display:none!important}.layui-show-v{visibility:visible!important}.layui-hide-v{visibility:hidden!important}@font-face{font-family:layui-icon;src:url(../font/iconfont.eot?v=256);src:url(../font/iconfont.eot?v=256#iefix) format('embedded-opentype'),url(../font/iconfont.woff2?v=256) format('woff2'),url(../font/iconfont.woff?v=256) format('woff'),url(../font/iconfont.ttf?v=256) format('truetype'),url(../font/iconfont.svg?v=256#layui-icon) format('svg')}.layui-icon{font-family:layui-icon!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.layui-icon-reply-fill:before{content:"\e611"}.layui-icon-set-fill:before{content:"\e614"}.layui-icon-menu-fill:before{content:"\e60f"}.layui-icon-search:before{content:"\e615"}.layui-icon-share:before{content:"\e641"}.layui-icon-set-sm:before{content:"\e620"}.layui-icon-engine:before{content:"\e628"}.layui-icon-close:before{content:"\1006"}.layui-icon-close-fill:before{content:"\1007"}.layui-icon-chart-screen:before{content:"\e629"}.layui-icon-star:before{content:"\e600"}.layui-icon-circle-dot:before{content:"\e617"}.layui-icon-chat:before{content:"\e606"}.layui-icon-release:before{content:"\e609"}.layui-icon-list:before{content:"\e60a"}.layui-icon-chart:before{content:"\e62c"}.layui-icon-ok-circle:before{content:"\1005"}.layui-icon-layim-theme:before{content:"\e61b"}.layui-icon-table:before{content:"\e62d"}.layui-icon-right:before{content:"\e602"}.layui-icon-left:before{content:"\e603"}.layui-icon-cart-simple:before{content:"\e698"}.layui-icon-face-cry:before{content:"\e69c"}.layui-icon-face-smile:before{content:"\e6af"}.layui-icon-survey:before{content:"\e6b2"}.layui-icon-tree:before{content:"\e62e"}.layui-icon-ie:before{content:"\e7bb"}.layui-icon-upload-circle:before{content:"\e62f"}.layui-icon-add-circle:before{content:"\e61f"}.layui-icon-download-circle:before{content:"\e601"}.layui-icon-templeate-1:before{content:"\e630"}.layui-icon-util:before{content:"\e631"}.layui-icon-face-surprised:before{content:"\e664"}.layui-icon-edit:before{content:"\e642"}.layui-icon-speaker:before{content:"\e645"}.layui-icon-down:before{content:"\e61a"}.layui-icon-file:before{content:"\e621"}.layui-icon-layouts:before{content:"\e632"}.layui-icon-rate-half:before{content:"\e6c9"}.layui-icon-add-circle-fine:before{content:"\e608"}.layui-icon-prev-circle:before{content:"\e633"}.layui-icon-read:before{content:"\e705"}.layui-icon-404:before{content:"\e61c"}.layui-icon-carousel:before{content:"\e634"}.layui-icon-help:before{content:"\e607"}.layui-icon-code-circle:before{content:"\e635"}.layui-icon-windows:before{content:"\e67f"}.layui-icon-water:before{content:"\e636"}.layui-icon-username:before{content:"\e66f"}.layui-icon-find-fill:before{content:"\e670"}.layui-icon-about:before{content:"\e60b"}.layui-icon-location:before{content:"\e715"}.layui-icon-up:before{content:"\e619"}.layui-icon-pause:before{content:"\e651"}.layui-icon-date:before{content:"\e637"}.layui-icon-layim-uploadfile:before{content:"\e61d"}.layui-icon-delete:before{content:"\e640"}.layui-icon-play:before{content:"\e652"}.layui-icon-top:before{content:"\e604"}.layui-icon-firefox:before{content:"\e686"}.layui-icon-friends:before{content:"\e612"}.layui-icon-refresh-3:before{content:"\e9aa"}.layui-icon-ok:before{content:"\e605"}.layui-icon-layer:before{content:"\e638"}.layui-icon-face-smile-fine:before{content:"\e60c"}.layui-icon-dollar:before{content:"\e659"}.layui-icon-group:before{content:"\e613"}.layui-icon-layim-download:before{content:"\e61e"}.layui-icon-picture-fine:before{content:"\e60d"}.layui-icon-link:before{content:"\e64c"}.layui-icon-diamond:before{content:"\e735"}.layui-icon-log:before{content:"\e60e"}.layui-icon-key:before{content:"\e683"}.layui-icon-rate-solid:before{content:"\e67a"}.layui-icon-fonts-del:before{content:"\e64f"}.layui-icon-unlink:before{content:"\e64d"}.layui-icon-fonts-clear:before{content:"\e639"}.layui-icon-triangle-r:before{content:"\e623"}.layui-icon-circle:before{content:"\e63f"}.layui-icon-radio:before{content:"\e643"}.layui-icon-align-center:before{content:"\e647"}.layui-icon-align-right:before{content:"\e648"}.layui-icon-align-left:before{content:"\e649"}.layui-icon-loading-1:before{content:"\e63e"}.layui-icon-return:before{content:"\e65c"}.layui-icon-fonts-strong:before{content:"\e62b"}.layui-icon-upload:before{content:"\e67c"}.layui-icon-dialogue:before{content:"\e63a"}.layui-icon-video:before{content:"\e6ed"}.layui-icon-headset:before{content:"\e6fc"}.layui-icon-cellphone-fine:before{content:"\e63b"}.layui-icon-add-1:before{content:"\e654"}.layui-icon-face-smile-b:before{content:"\e650"}.layui-icon-fonts-html:before{content:"\e64b"}.layui-icon-screen-full:before{content:"\e622"}.layui-icon-form:before{content:"\e63c"}.layui-icon-cart:before{content:"\e657"}.layui-icon-camera-fill:before{content:"\e65d"}.layui-icon-tabs:before{content:"\e62a"}.layui-icon-heart-fill:before{content:"\e68f"}.layui-icon-fonts-code:before{content:"\e64e"}.layui-icon-ios:before{content:"\e680"}.layui-icon-at:before{content:"\e687"}.layui-icon-fire:before{content:"\e756"}.layui-icon-set:before{content:"\e716"}.layui-icon-fonts-u:before{content:"\e646"}.layui-icon-triangle-d:before{content:"\e625"}.layui-icon-tips:before{content:"\e702"}.layui-icon-picture:before{content:"\e64a"}.layui-icon-more-vertical:before{content:"\e671"}.layui-icon-bluetooth:before{content:"\e689"}.layui-icon-flag:before{content:"\e66c"}.layui-icon-loading:before{content:"\e63d"}.layui-icon-fonts-i:before{content:"\e644"}.layui-icon-refresh-1:before{content:"\e666"}.layui-icon-rmb:before{content:"\e65e"}.layui-icon-addition:before{content:"\e624"}.layui-icon-home:before{content:"\e68e"}.layui-icon-time:before{content:"\e68d"}.layui-icon-user:before{content:"\e770"}.layui-icon-notice:before{content:"\e667"}.layui-icon-chrome:before{content:"\e68a"}.layui-icon-edge:before{content:"\e68b"}.layui-icon-login-weibo:before{content:"\e675"}.layui-icon-voice:before{content:"\e688"}.layui-icon-upload-drag:before{content:"\e681"}.layui-icon-login-qq:before{content:"\e676"}.layui-icon-snowflake:before{content:"\e6b1"}.layui-icon-heart:before{content:"\e68c"}.layui-icon-logout:before{content:"\e682"}.layui-icon-file-b:before{content:"\e655"}.layui-icon-template:before{content:"\e663"}.layui-icon-transfer:before{content:"\e691"}.layui-icon-auz:before{content:"\e672"}.layui-icon-console:before{content:"\e665"}.layui-icon-app:before{content:"\e653"}.layui-icon-prev:before{content:"\e65a"}.layui-icon-website:before{content:"\e7ae"}.layui-icon-next:before{content:"\e65b"}.layui-icon-component:before{content:"\e857"}.layui-icon-android:before{content:"\e684"}.layui-icon-more:before{content:"\e65f"}.layui-icon-login-wechat:before{content:"\e677"}.layui-icon-shrink-right:before{content:"\e668"}.layui-icon-spread-left:before{content:"\e66b"}.layui-icon-camera:before{content:"\e660"}.layui-icon-note:before{content:"\e66e"}.layui-icon-refresh:before{content:"\e669"}.layui-icon-female:before{content:"\e661"}.layui-icon-male:before{content:"\e662"}.layui-icon-screen-restore:before{content:"\e758"}.layui-icon-password:before{content:"\e673"}.layui-icon-senior:before{content:"\e674"}.layui-icon-theme:before{content:"\e66a"}.layui-icon-tread:before{content:"\e6c5"}.layui-icon-praise:before{content:"\e6c6"}.layui-icon-star-fill:before{content:"\e658"}.layui-icon-rate:before{content:"\e67b"}.layui-icon-template-1:before{content:"\e656"}.layui-icon-vercode:before{content:"\e679"}.layui-icon-service:before{content:"\e626"}.layui-icon-cellphone:before{content:"\e678"}.layui-icon-print:before{content:"\e66d"}.layui-icon-cols:before{content:"\e610"}.layui-icon-wifi:before{content:"\e7e0"}.layui-icon-export:before{content:"\e67d"}.layui-icon-rss:before{content:"\e808"}.layui-icon-slider:before{content:"\e714"}.layui-icon-email:before{content:"\e618"}.layui-icon-subtraction:before{content:"\e67e"}.layui-icon-mike:before{content:"\e6dc"}.layui-icon-light:before{content:"\e748"}.layui-icon-gift:before{content:"\e627"}.layui-icon-mute:before{content:"\e685"}.layui-icon-reduce-circle:before{content:"\e616"}.layui-icon-music:before{content:"\e690"}.layui-main{position:relative;width:1160px;margin:0 auto}.layui-header{position:relative;z-index:1000;height:60px}.layui-header a:hover{transition:all .5s;-webkit-transition:all .5s}.layui-side{position:fixed;left:0;top:0;bottom:0;z-index:999;width:200px;overflow-x:hidden}.layui-side-scroll{position:relative;width:220px;height:100%;overflow-x:hidden}.layui-body{position:relative;left:200px;right:0;top:0;bottom:0;z-index:900;width:auto;box-sizing:border-box}.layui-layout-body{overflow-x:hidden}.layui-layout-admin .layui-header{position:fixed;top:0;left:0;right:0;background-color:#23262e}.layui-layout-admin .layui-side{top:60px;width:200px;overflow-x:hidden}.layui-layout-admin .layui-body{position:absolute;top:60px;padding-bottom:44px}.layui-layout-admin .layui-main{width:auto;margin:0 15px}.layui-layout-admin .layui-footer{position:fixed;left:200px;right:0;bottom:0;z-index:990;height:44px;line-height:44px;padding:0 15px;box-shadow:-1px 0 4px rgb(0 0 0 / 12%);background-color:#fafafa}.layui-layout-admin .layui-logo{position:absolute;left:0;top:0;width:200px;height:100%;line-height:60px;text-align:center;color:#009688;font-size:16px;box-shadow:0 1px 2px 0 rgb(0 0 0 / 15%)}.layui-layout-admin .layui-header .layui-nav{background:0 0}.layui-layout-left{position:absolute!important;left:200px;top:0}.layui-layout-right{position:absolute!important;right:0;top:0}.layui-container{position:relative;margin:0 auto;box-sizing:border-box}.layui-fluid{position:relative;margin:0 auto;padding:0 15px}.layui-row:after,.layui-row:before{content:"";display:block;clear:both}.layui-col-lg1,.layui-col-lg10,.layui-col-lg11,.layui-col-lg12,.layui-col-lg2,.layui-col-lg3,.layui-col-lg4,.layui-col-lg5,.layui-col-lg6,.layui-col-lg7,.layui-col-lg8,.layui-col-lg9,.layui-col-md1,.layui-col-md10,.layui-col-md11,.layui-col-md12,.layui-col-md2,.layui-col-md3,.layui-col-md4,.layui-col-md5,.layui-col-md6,.layui-col-md7,.layui-col-md8,.layui-col-md9,.layui-col-sm1,.layui-col-sm10,.layui-col-sm11,.layui-col-sm12,.layui-col-sm2,.layui-col-sm3,.layui-col-sm4,.layui-col-sm5,.layui-col-sm6,.layui-col-sm7,.layui-col-sm8,.layui-col-sm9,.layui-col-xs1,.layui-col-xs10,.layui-col-xs11,.layui-col-xs12,.layui-col-xs2,.layui-col-xs3,.layui-col-xs4,.layui-col-xs5,.layui-col-xs6,.layui-col-xs7,.layui-col-xs8,.layui-col-xs9{position:relative;display:block;box-sizing:border-box}.layui-col-xs1,.layui-col-xs10,.layui-col-xs11,.layui-col-xs12,.layui-col-xs2,.layui-col-xs3,.layui-col-xs4,.layui-col-xs5,.layui-col-xs6,.layui-col-xs7,.layui-col-xs8,.layui-col-xs9{float:left}.layui-col-xs1{width:8.33333333%}.layui-col-xs2{width:16.66666667%}.layui-col-xs3{width:25%}.layui-col-xs4{width:33.33333333%}.layui-col-xs5{width:41.66666667%}.layui-col-xs6{width:50%}.layui-col-xs7{width:58.33333333%}.layui-col-xs8{width:66.66666667%}.layui-col-xs9{width:75%}.layui-col-xs10{width:83.33333333%}.layui-col-xs11{width:91.66666667%}.layui-col-xs12{width:100%}.layui-col-xs-offset1{margin-left:8.33333333%}.layui-col-xs-offset2{margin-left:16.66666667%}.layui-col-xs-offset3{margin-left:25%}.layui-col-xs-offset4{margin-left:33.33333333%}.layui-col-xs-offset5{margin-left:41.66666667%}.layui-col-xs-offset6{margin-left:50%}.layui-col-xs-offset7{margin-left:58.33333333%}.layui-col-xs-offset8{margin-left:66.66666667%}.layui-col-xs-offset9{margin-left:75%}.layui-col-xs-offset10{margin-left:83.33333333%}.layui-col-xs-offset11{margin-left:91.66666667%}.layui-col-xs-offset12{margin-left:100%}@media screen and (max-width:767.98px){.layui-container{padding:0 15px}.layui-hide-xs{display:none!important}.layui-show-xs-block{display:block!important}.layui-show-xs-inline{display:inline!important}.layui-show-xs-inline-block{display:inline-block!important}}@media screen and (min-width:768px){.layui-container{width:720px}.layui-hide-sm{display:none!important}.layui-show-sm-block{display:block!important}.layui-show-sm-inline{display:inline!important}.layui-show-sm-inline-block{display:inline-block!important}.layui-col-sm1,.layui-col-sm10,.layui-col-sm11,.layui-col-sm12,.layui-col-sm2,.layui-col-sm3,.layui-col-sm4,.layui-col-sm5,.layui-col-sm6,.layui-col-sm7,.layui-col-sm8,.layui-col-sm9{float:left}.layui-col-sm1{width:8.33333333%}.layui-col-sm2{width:16.66666667%}.layui-col-sm3{width:25%}.layui-col-sm4{width:33.33333333%}.layui-col-sm5{width:41.66666667%}.layui-col-sm6{width:50%}.layui-col-sm7{width:58.33333333%}.layui-col-sm8{width:66.66666667%}.layui-col-sm9{width:75%}.layui-col-sm10{width:83.33333333%}.layui-col-sm11{width:91.66666667%}.layui-col-sm12{width:100%}.layui-col-sm-offset1{margin-left:8.33333333%}.layui-col-sm-offset2{margin-left:16.66666667%}.layui-col-sm-offset3{margin-left:25%}.layui-col-sm-offset4{margin-left:33.33333333%}.layui-col-sm-offset5{margin-left:41.66666667%}.layui-col-sm-offset6{margin-left:50%}.layui-col-sm-offset7{margin-left:58.33333333%}.layui-col-sm-offset8{margin-left:66.66666667%}.layui-col-sm-offset9{margin-left:75%}.layui-col-sm-offset10{margin-left:83.33333333%}.layui-col-sm-offset11{margin-left:91.66666667%}.layui-col-sm-offset12{margin-left:100%}}@media screen and (min-width:992px){.layui-container{width:960px}.layui-hide-md{display:none!important}.layui-show-md-block{display:block!important}.layui-show-md-inline{display:inline!important}.layui-show-md-inline-block{display:inline-block!important}.layui-col-md1,.layui-col-md10,.layui-col-md11,.layui-col-md12,.layui-col-md2,.layui-col-md3,.layui-col-md4,.layui-col-md5,.layui-col-md6,.layui-col-md7,.layui-col-md8,.layui-col-md9{float:left}.layui-col-md1{width:8.33333333%}.layui-col-md2{width:16.66666667%}.layui-col-md3{width:25%}.layui-col-md4{width:33.33333333%}.layui-col-md5{width:41.66666667%}.layui-col-md6{width:50%}.layui-col-md7{width:58.33333333%}.layui-col-md8{width:66.66666667%}.layui-col-md9{width:75%}.layui-col-md10{width:83.33333333%}.layui-col-md11{width:91.66666667%}.layui-col-md12{width:100%}.layui-col-md-offset1{margin-left:8.33333333%}.layui-col-md-offset2{margin-left:16.66666667%}.layui-col-md-offset3{margin-left:25%}.layui-col-md-offset4{margin-left:33.33333333%}.layui-col-md-offset5{margin-left:41.66666667%}.layui-col-md-offset6{margin-left:50%}.layui-col-md-offset7{margin-left:58.33333333%}.layui-col-md-offset8{margin-left:66.66666667%}.layui-col-md-offset9{margin-left:75%}.layui-col-md-offset10{margin-left:83.33333333%}.layui-col-md-offset11{margin-left:91.66666667%}.layui-col-md-offset12{margin-left:100%}}@media screen and (min-width:1200px){.layui-container{width:1150px}.layui-hide-lg{display:none!important}.layui-show-lg-block{display:block!important}.layui-show-lg-inline{display:inline!important}.layui-show-lg-inline-block{display:inline-block!important}.layui-col-lg1,.layui-col-lg10,.layui-col-lg11,.layui-col-lg12,.layui-col-lg2,.layui-col-lg3,.layui-col-lg4,.layui-col-lg5,.layui-col-lg6,.layui-col-lg7,.layui-col-lg8,.layui-col-lg9{float:left}.layui-col-lg1{width:8.33333333%}.layui-col-lg2{width:16.66666667%}.layui-col-lg3{width:25%}.layui-col-lg4{width:33.33333333%}.layui-col-lg5{width:41.66666667%}.layui-col-lg6{width:50%}.layui-col-lg7{width:58.33333333%}.layui-col-lg8{width:66.66666667%}.layui-col-lg9{width:75%}.layui-col-lg10{width:83.33333333%}.layui-col-lg11{width:91.66666667%}.layui-col-lg12{width:100%}.layui-col-lg-offset1{margin-left:8.33333333%}.layui-col-lg-offset2{margin-left:16.66666667%}.layui-col-lg-offset3{margin-left:25%}.layui-col-lg-offset4{margin-left:33.33333333%}.layui-col-lg-offset5{margin-left:41.66666667%}.layui-col-lg-offset6{margin-left:50%}.layui-col-lg-offset7{margin-left:58.33333333%}.layui-col-lg-offset8{margin-left:66.66666667%}.layui-col-lg-offset9{margin-left:75%}.layui-col-lg-offset10{margin-left:83.33333333%}.layui-col-lg-offset11{margin-left:91.66666667%}.layui-col-lg-offset12{margin-left:100%}}.layui-col-space1{margin:-.5px}.layui-col-space1>*{padding:.5px}.layui-col-space2{margin:-1px}.layui-col-space2>*{padding:1px}.layui-col-space4{margin:-2px}.layui-col-space4>*{padding:2px}.layui-col-space5{margin:-2.5px}.layui-col-space5>*{padding:2.5px}.layui-col-space6{margin:-3px}.layui-col-space6>*{padding:3px}.layui-col-space8{margin:-4px}.layui-col-space8>*{padding:4px}.layui-col-space10{margin:-5px}.layui-col-space10>*{padding:5px}.layui-col-space12{margin:-6px}.layui-col-space12>*{padding:6px}.layui-col-space14{margin:-7px}.layui-col-space14>*{padding:7px}.layui-col-space15{margin:-7.5px}.layui-col-space15>*{padding:7.5px}.layui-col-space16{margin:-8px}.layui-col-space16>*{padding:8px}.layui-col-space18{margin:-9px}.layui-col-space18>*{padding:9px}.layui-col-space20{margin:-10px}.layui-col-space20>*{padding:10px}.layui-col-space22{margin:-11px}.layui-col-space22>*{padding:11px}.layui-col-space24{margin:-12px}.layui-col-space24>*{padding:12px}.layui-col-space25{margin:-12.5px}.layui-col-space25>*{padding:12.5px}.layui-col-space26{margin:-13px}.layui-col-space26>*{padding:13px}.layui-col-space28{margin:-14px}.layui-col-space28>*{padding:14px}.layui-col-space30{margin:-15px}.layui-col-space30>*{padding:15px}.layui-btn,.layui-input,.layui-select,.layui-textarea,.layui-upload-button{outline:0;-webkit-appearance:none;transition:all .3s;-webkit-transition:all .3s;box-sizing:border-box}.layui-elem-quote{margin-bottom:10px;padding:15px;line-height:1.6;border-left:5px solid #5fb878;border-radius:0 2px 2px 0;background-color:#fafafa}.layui-quote-nm{border-style:solid;border-width:1px;border-left-width:5px;background:0 0}.layui-elem-field{margin-bottom:10px;padding:0;border-width:1px;border-style:solid}.layui-elem-field legend{margin-left:20px;padding:0 10px;font-size:20px;font-weight:300}.layui-field-title{margin:10px 0 20px;border-width:0;border-top-width:1px}.layui-field-box{padding:15px}.layui-field-title .layui-field-box{padding:10px 0}.layui-progress{position:relative;height:6px;border-radius:20px;background-color:#eee}.layui-progress-bar{position:absolute;left:0;top:0;width:0;max-width:100%;height:6px;border-radius:20px;text-align:right;background-color:#5fb878;transition:all .3s;-webkit-transition:all .3s}.layui-progress-big,.layui-progress-big .layui-progress-bar{height:18px;line-height:18px}.layui-progress-text{position:relative;top:-20px;line-height:18px;font-size:12px;color:#5f5f5f}.layui-progress-big .layui-progress-text{position:static;padding:0 10px;color:#fff}.layui-collapse{border-width:1px;border-style:solid;border-radius:2px}.layui-colla-content,.layui-colla-item{border-top-width:1px;border-top-style:solid}.layui-colla-item:first-child{border-top:none}.layui-colla-title{position:relative;height:42px;line-height:42px;padding:0 15px 0 35px;color:#333;background-color:#fafafa;cursor:pointer;font-size:14px;overflow:hidden}.layui-colla-content{display:none;padding:10px 15px;line-height:1.6;color:#5f5f5f}.layui-colla-icon{position:absolute;left:15px;top:0;font-size:14px}.layui-card{margin-bottom:15px;border-radius:2px;background-color:#fff;box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.layui-card:last-child{margin-bottom:0}.layui-card-header{position:relative;height:42px;line-height:42px;padding:0 15px;border-bottom:1px solid #f6f6f6;color:#333;border-radius:2px 2px 0 0;font-size:14px}.layui-card-body{position:relative;padding:10px 15px;line-height:24px}.layui-card-body[pad15]{padding:15px}.layui-card-body[pad20]{padding:20px}.layui-card-body .layui-table{margin:5px 0}.layui-card .layui-tab{margin:0}.layui-panel{position:relative;border-width:1px;border-style:solid;border-radius:2px;box-shadow:1px 1px 4px rgb(0 0 0 / 8%);background-color:#fff;color:#5f5f5f}.layui-panel-window{position:relative;padding:15px;border-radius:0;border-top:5px solid #eee;background-color:#fff}.layui-auxiliar-moving{position:fixed;left:0;right:0;top:0;bottom:0;width:100%;height:100%;background:0 0;z-index:9999999999}.layui-bg-red{background-color:#ff5722!important;color:#fff!important}.layui-bg-orange{background-color:#ffb800!important;color:#fff!important}.layui-bg-green{background-color:#009688!important;color:#fff!important}.layui-bg-cyan{background-color:#2f4056!important;color:#fff!important}.layui-bg-blue{background-color:#1e9fff!important;color:#fff!important}.layui-bg-black{background-color:#393d49!important;color:#fff!important}.layui-bg-gray{background-color:#fafafa!important;color:#5f5f5f!important}.layui-badge-rim,.layui-border,.layui-colla-content,.layui-colla-item,.layui-collapse,.layui-elem-field,.layui-form-pane .layui-form-item[pane],.layui-form-pane .layui-form-label,.layui-input,.layui-layedit,.layui-layedit-tool,.layui-panel,.layui-quote-nm,.layui-select,.layui-tab-bar,.layui-tab-card,.layui-tab-title,.layui-tab-title .layui-this:after,.layui-textarea{border-color:#eee}.layui-border{border-width:1px;border-style:solid;color:#5f5f5f!important}.layui-border-red{border-width:1px;border-style:solid;border-color:#ff5722!important;color:#ff5722!important}.layui-border-orange{border-width:1px;border-style:solid;border-color:#ffb800!important;color:#ffb800!important}.layui-border-green{border-width:1px;border-style:solid;border-color:#009688!important;color:#009688!important}.layui-border-cyan{border-width:1px;border-style:solid;border-color:#2f4056!important;color:#2f4056!important}.layui-border-blue{border-width:1px;border-style:solid;border-color:#1e9fff!important;color:#1e9fff!important}.layui-border-black{border-width:1px;border-style:solid;border-color:#393d49!important;color:#393d49!important}.layui-timeline-item:before{background-color:#eee}.layui-text{line-height:1.6;font-size:14px;color:#5f5f5f}.layui-text h1,.layui-text h2,.layui-text h3,.layui-text h4,.layui-text h5,.layui-text h6{font-weight:500;color:#333}.layui-text h1{font-size:32px}.layui-text h2{font-size:24px}.layui-text h3{font-size:18px}.layui-text h4{font-size:16px}.layui-text h5{font-size:14px}.layui-text h6{font-size:13px}.layui-text a:not(.layui-btn){color:#01aaed}.layui-text a:not(.layui-btn):hover{text-decoration:underline}.layui-text ol,.layui-text ul{padding:5px 0 5px 15px}.layui-text ul li{margin-top:5px;list-style-type:disc}.layui-text ol li{margin-top:5px;list-style-type:decimal}.layui-text em,.layui-word-aux{color:#999!important;padding-left:5px!important;padding-right:5px!important}.layui-text p{margin:15px 0}.layui-text p:first-child{margin-top:0}.layui-text p:last-child{margin-bottom:0}.layui-text blockquote:not(.layui-elem-quote){padding:5px 15px;border-left:5px solid #eee}.layui-text pre:not(.layui-code){padding:15px;font-family:Lucida Console,Consolas,Courier New;background-color:#fafafa}.layui-font-12{font-size:12px!important}.layui-font-14{font-size:14px!important}.layui-font-16{font-size:16px!important}.layui-font-18{font-size:18px!important}.layui-font-20{font-size:20px!important}.layui-font-red{color:#ff5722!important}.layui-font-orange{color:#ffb800!important}.layui-font-green{color:#009688!important}.layui-font-cyan{color:#2f4056!important}.layui-font-blue{color:#01aaed!important}.layui-font-black{color:#000!important}.layui-font-gray{color:#c2c2c2!important}.layui-btn{display:inline-block;vertical-align:middle;height:38px;line-height:38px;border:1px solid transparent;padding:0 18px;background-color:#009688;color:#fff;white-space:nowrap;text-align:center;font-size:14px;border-radius:2px;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-btn:hover{opacity:.8;filter:alpha(opacity=80);color:#fff}.layui-btn:active{opacity:1;filter:alpha(opacity=100)}.layui-btn+.layui-btn{margin-left:10px}.layui-btn-container{font-size:0}.layui-btn-container .layui-btn{margin-right:10px;margin-bottom:10px}.layui-btn-container .layui-btn+.layui-btn{margin-left:0}.layui-table .layui-btn-container .layui-btn{margin-bottom:9px}.layui-btn-radius{border-radius:100px}.layui-btn .layui-icon{padding:0 2px;vertical-align:middle\0;vertical-align:bottom}.layui-btn-primary{border-color:#d2d2d2;background:0 0;color:#5f5f5f}.layui-btn-primary:hover{border-color:#009688;color:#333}.layui-btn-normal{background-color:#1e9fff}.layui-btn-warm{background-color:#ffb800}.layui-btn-danger{background-color:#ff5722}.layui-btn-checked{background-color:#5fb878}.layui-btn-disabled,.layui-btn-disabled:active,.layui-btn-disabled:hover{border-color:#eee!important;background-color:#fbfbfb!important;color:#d2d2d2!important;cursor:not-allowed!important;opacity:1}.layui-btn-lg{height:44px;line-height:44px;padding:0 25px;font-size:16px}.layui-btn-sm{height:30px;line-height:30px;padding:0 10px;font-size:12px}.layui-btn-xs{height:22px;line-height:22px;padding:0 5px;font-size:12px}.layui-btn-xs i{font-size:12px!important}.layui-btn-group{display:inline-block;vertical-align:middle;font-size:0}.layui-btn-group .layui-btn{margin-left:0!important;margin-right:0!important;border-left:1px solid rgba(255,255,255,.5);border-radius:0}.layui-btn-group .layui-btn-primary{border-left:none}.layui-btn-group .layui-btn-primary:hover{border-color:#d2d2d2;color:#009688}.layui-btn-group .layui-btn:first-child{border-left:none;border-radius:2px 0 0 2px}.layui-btn-group .layui-btn-primary:first-child{border-left:1px solid #d2d2d2}.layui-btn-group .layui-btn:last-child{border-radius:0 2px 2px 0}.layui-btn-group .layui-btn+.layui-btn{margin-left:0}.layui-btn-group+.layui-btn-group{margin-left:10px}.layui-btn-fluid{width:100%}.layui-input,.layui-select,.layui-textarea{height:38px;line-height:1.3;line-height:38px\9;border-width:1px;border-style:solid;background-color:#fff;color:rgba(0,0,0,.85);border-radius:2px}.layui-input::-webkit-input-placeholder,.layui-select::-webkit-input-placeholder,.layui-textarea::-webkit-input-placeholder{line-height:1.3}.layui-input,.layui-textarea{display:block;width:100%;padding-left:10px}.layui-input:hover,.layui-textarea:hover{border-color:#eee!important}.layui-input:focus,.layui-textarea:focus{border-color:#d2d2d2!important}.layui-textarea{position:relative;min-height:100px;height:auto;line-height:20px;padding:6px 10px;resize:vertical}.layui-select{padding:0 10px}.layui-form input[type=checkbox],.layui-form input[type=radio],.layui-form select{display:none}.layui-form [lay-ignore]{display:initial}.layui-form-item{margin-bottom:15px;clear:both;*zoom:1}.layui-form-item:after{content:'\20';clear:both;*zoom:1;display:block;height:0}.layui-form-label{position:relative;float:left;display:block;padding:9px 15px;width:80px;font-weight:400;line-height:20px;text-align:right}.layui-form-label-col{display:block;float:none;padding:9px 0;line-height:20px;text-align:left}.layui-form-item .layui-inline{margin-bottom:5px;margin-right:10px}.layui-input-block,.layui-input-inline{position:relative}.layui-input-block{margin-left:110px;min-height:36px}.layui-input-inline{display:inline-block;vertical-align:middle}.layui-form-item .layui-input-inline{float:left;width:190px;margin-right:10px}.layui-form-text .layui-input-inline{width:auto}.layui-form-mid{position:relative;float:left;display:block;padding:9px 0!important;line-height:20px;margin-right:10px}.layui-form-danger+.layui-form-select .layui-input,.layui-form-danger:focus{border-color:#ff5722!important}.layui-form-select{position:relative}.layui-form-select .layui-input{padding-right:30px;cursor:pointer}.layui-form-select .layui-edge{position:absolute;right:10px;top:50%;margin-top:-3px;cursor:pointer;border-width:6px;border-top-color:#c2c2c2;border-top-style:solid;transition:all .3s;-webkit-transition:all .3s}.layui-form-select dl{display:none;position:absolute;left:0;top:42px;padding:5px 0;z-index:899;min-width:100%;border:1px solid #eee;max-height:300px;overflow-y:auto;background-color:#fff;border-radius:2px;box-shadow:1px 1px 4px rgb(0 0 0 / 8%);box-sizing:border-box}.layui-form-select dl dd,.layui-form-select dl dt{padding:0 10px;line-height:36px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.layui-form-select dl dt{font-size:12px;color:#999}.layui-form-select dl dd{cursor:pointer}.layui-form-select dl dd:hover{background-color:#f6f6f6;-webkit-transition:.5s all;transition:.5s all}.layui-form-select .layui-select-group dd{padding-left:20px}.layui-form-select dl dd.layui-select-tips{padding-left:10px!important;color:#999}.layui-form-select dl dd.layui-this{background-color:#5fb878;color:#fff}.layui-form-select dl dd.layui-disabled{background-color:#fff}.layui-form-selected dl{display:block}.layui-form-selected .layui-edge{margin-top:-9px;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.layui-form-selected .layui-edge{margin-top:-3px\0}:root .layui-form-selected .layui-edge{margin-top:-9px\0/IE9}.layui-form-selectup dl{top:auto;bottom:42px}.layui-select-none{margin:5px 0;text-align:center;color:#999}.layui-select-disabled .layui-disabled{border-color:#eee!important}.layui-select-disabled .layui-edge{border-top-color:#d2d2d2}.layui-form-checkbox{position:relative;display:inline-block;vertical-align:middle;height:30px;line-height:30px;margin-right:10px;padding-right:30px;background-color:#fff;cursor:pointer;font-size:0;-webkit-transition:.1s linear;transition:.1s linear;box-sizing:border-box}.layui-form-checkbox *{display:inline-block;vertical-align:middle}.layui-form-checkbox span{padding:0 10px;height:100%;font-size:14px;border-radius:2px 0 0 2px;background-color:#d2d2d2;color:#fff;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.layui-form-checkbox:hover span{background-color:#c2c2c2}.layui-form-checkbox i{position:absolute;right:0;top:0;width:30px;height:28px;border:1px solid #d2d2d2;border-left:none;border-radius:0 2px 2px 0;color:#fff;font-size:20px;text-align:center}.layui-form-checkbox:hover i{border-color:#c2c2c2;color:#c2c2c2}.layui-form-checked,.layui-form-checked:hover{border-color:#5fb878}.layui-form-checked span,.layui-form-checked:hover span{background-color:#5fb878}.layui-form-checked i,.layui-form-checked:hover i{color:#5fb878}.layui-form-item .layui-form-checkbox{margin-top:4px}.layui-form-checkbox[lay-skin=primary]{height:auto!important;line-height:normal!important;min-width:18px;min-height:18px;border:none!important;margin-right:0;padding-left:28px;padding-right:0;background:0 0}.layui-form-checkbox[lay-skin=primary] span{padding-left:0;padding-right:15px;line-height:18px;background:0 0;color:#5f5f5f}.layui-form-checkbox[lay-skin=primary] i{right:auto;left:0;width:16px;height:16px;line-height:16px;border:1px solid #d2d2d2;font-size:12px;border-radius:2px;background-color:#fff;-webkit-transition:.1s linear;transition:.1s linear}.layui-form-checkbox[lay-skin=primary]:hover i{border-color:#5fb878;color:#fff}.layui-form-checked[lay-skin=primary] i{border-color:#5fb878!important;background-color:#5fb878;color:#fff}.layui-checkbox-disabled[lay-skin=primary] span{background:0 0!important;color:#c2c2c2!important}.layui-form-checked.layui-checkbox-disabled[lay-skin=primary] i{background:#eee!important;border-color:#eee!important}.layui-checkbox-disabled[lay-skin=primary]:hover i{border-color:#d2d2d2}.layui-form-item .layui-form-checkbox[lay-skin=primary]{margin-top:10px}.layui-form-switch{position:relative;display:inline-block;vertical-align:middle;height:22px;line-height:22px;min-width:35px;padding:0 5px;margin-top:8px;border:1px solid #d2d2d2;border-radius:20px;cursor:pointer;background-color:#fff;-webkit-transition:.1s linear;transition:.1s linear}.layui-form-switch i{position:absolute;left:5px;top:3px;width:16px;height:16px;border-radius:20px;background-color:#d2d2d2;-webkit-transition:.1s linear;transition:.1s linear}.layui-form-switch em{position:relative;top:0;width:25px;margin-left:21px;padding:0!important;text-align:center!important;color:#999!important;font-style:normal!important;font-size:12px}.layui-form-onswitch{border-color:#5fb878;background-color:#5fb878}.layui-form-onswitch i{left:100%;margin-left:-21px;background-color:#fff}.layui-form-onswitch em{margin-left:5px;margin-right:21px;color:#fff!important}.layui-checkbox-disabled{border-color:#eee!important}.layui-checkbox-disabled span{background-color:#eee!important}.layui-checkbox-disabled i{border-color:#eee!important}.layui-checkbox-disabled em{color:#d2d2d2!important}.layui-checkbox-disabled:hover i{color:#fff!important}[lay-radio]{display:none}.layui-form-radio{display:inline-block;vertical-align:middle;line-height:28px;margin:6px 10px 0 0;padding-right:10px;cursor:pointer;font-size:0}.layui-form-radio *{display:inline-block;vertical-align:middle;font-size:14px}.layui-form-radio>i{margin-right:8px;font-size:22px;color:#c2c2c2}.layui-form-radio:hover *,.layui-form-radioed,.layui-form-radioed>i{color:#5fb878}.layui-radio-disabled>i{color:#eee!important}.layui-radio-disabled *{color:#c2c2c2!important}.layui-form-pane .layui-form-label{width:110px;padding:8px 15px;height:38px;line-height:20px;border-width:1px;border-style:solid;border-radius:2px 0 0 2px;text-align:center;background-color:#fafafa;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;box-sizing:border-box}.layui-form-pane .layui-input-inline{margin-left:-1px}.layui-form-pane .layui-input-block{margin-left:110px;left:-1px}.layui-form-pane .layui-input{border-radius:0 2px 2px 0}.layui-form-pane .layui-form-text .layui-form-label{float:none;width:100%;border-radius:2px;box-sizing:border-box;text-align:left}.layui-form-pane .layui-form-text .layui-input-inline{display:block;margin:0;top:-1px;clear:both}.layui-form-pane .layui-form-text .layui-input-block{margin:0;left:0;top:-1px}.layui-form-pane .layui-form-text .layui-textarea{min-height:100px;border-radius:0 0 2px 2px}.layui-form-pane .layui-form-checkbox{margin:4px 0 4px 10px}.layui-form-pane .layui-form-radio,.layui-form-pane .layui-form-switch{margin-top:6px;margin-left:10px}.layui-form-pane .layui-form-item[pane]{position:relative;border-width:1px;border-style:solid}.layui-form-pane .layui-form-item[pane] .layui-form-label{position:absolute;left:0;top:0;height:100%;border-width:0;border-right-width:1px}.layui-form-pane .layui-form-item[pane] .layui-input-inline{margin-left:110px}@media screen and (max-width:450px){.layui-form-item .layui-form-label{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-form-item .layui-inline{display:block;margin-right:0;margin-bottom:20px;clear:both}.layui-form-item .layui-inline:after{content:'\20';clear:both;display:block;height:0}.layui-form-item .layui-input-inline{display:block;float:none;left:-3px;width:auto!important;margin:0 0 10px 112px}.layui-form-item .layui-input-inline+.layui-form-mid{margin-left:110px;top:-5px;padding:0}.layui-form-item .layui-form-checkbox{margin-right:5px;margin-bottom:5px}}.layui-layedit{border-width:1px;border-style:solid;border-radius:2px}.layui-layedit-tool{padding:3px 5px;border-bottom-width:1px;border-bottom-style:solid;font-size:0}.layedit-tool-fixed{position:fixed;top:0;border-top:1px solid #eee}.layui-layedit-tool .layedit-tool-mid,.layui-layedit-tool .layui-icon{display:inline-block;vertical-align:middle;text-align:center;font-size:14px}.layui-layedit-tool .layui-icon{position:relative;width:32px;height:30px;line-height:30px;margin:3px 5px;border-radius:2px;color:#777;cursor:pointer;border-radius:2px}.layui-layedit-tool .layui-icon:hover{color:#393d49}.layui-layedit-tool .layui-icon:active{color:#000}.layui-layedit-tool .layedit-tool-active{background-color:#eee;color:#000}.layui-layedit-tool .layui-disabled,.layui-layedit-tool .layui-disabled:hover{color:#d2d2d2;cursor:not-allowed}.layui-layedit-tool .layedit-tool-mid{width:1px;height:18px;margin:0 10px;background-color:#d2d2d2}.layedit-tool-html{width:50px!important;font-size:30px!important}.layedit-tool-b,.layedit-tool-code,.layedit-tool-help{font-size:16px!important}.layedit-tool-d,.layedit-tool-face,.layedit-tool-image,.layedit-tool-unlink{font-size:18px!important}.layedit-tool-image input{position:absolute;font-size:0;left:0;top:0;width:100%;height:100%;opacity:.01;filter:Alpha(opacity=1);cursor:pointer}.layui-layedit-iframe iframe{display:block;width:100%}#LAY_layedit_code{overflow:hidden}.layui-laypage{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;margin:10px 0;font-size:0}.layui-laypage>a:first-child,.layui-laypage>a:first-child em{border-radius:2px 0 0 2px}.layui-laypage>a:last-child,.layui-laypage>a:last-child em{border-radius:0 2px 2px 0}.layui-laypage>:first-child{margin-left:0!important}.layui-laypage>:last-child{margin-right:0!important}.layui-laypage a,.layui-laypage button,.layui-laypage input,.layui-laypage select,.layui-laypage span{border:1px solid #eee}.layui-laypage a,.layui-laypage span{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding:0 15px;height:28px;line-height:28px;margin:0 -1px 5px 0;background-color:#fff;color:#333;font-size:12px}.layui-laypage a:hover{color:#009688}.layui-laypage em{font-style:normal}.layui-laypage .layui-laypage-spr{color:#999;font-weight:700}.layui-laypage a{text-decoration:none}.layui-laypage .layui-laypage-curr{position:relative}.layui-laypage .layui-laypage-curr em{position:relative;color:#fff}.layui-laypage .layui-laypage-curr .layui-laypage-em{position:absolute;left:-1px;top:-1px;padding:1px;width:100%;height:100%;background-color:#009688}.layui-laypage-em{border-radius:2px}.layui-laypage-next em,.layui-laypage-prev em{font-family:Sim sun;font-size:16px}.layui-laypage .layui-laypage-count,.layui-laypage .layui-laypage-limits,.layui-laypage .layui-laypage-refresh,.layui-laypage .layui-laypage-skip{margin-left:10px;margin-right:10px;padding:0;border:none}.layui-laypage .layui-laypage-limits,.layui-laypage .layui-laypage-refresh{vertical-align:top}.layui-laypage .layui-laypage-refresh i{font-size:18px;cursor:pointer}.layui-laypage select{height:22px;padding:3px;border-radius:2px;cursor:pointer}.layui-laypage .layui-laypage-skip{height:30px;line-height:30px;color:#999}.layui-laypage button,.layui-laypage input{height:30px;line-height:30px;border-radius:2px;vertical-align:top;background-color:#fff;box-sizing:border-box}.layui-laypage input{display:inline-block;width:40px;margin:0 10px;padding:0 3px;text-align:center}.layui-laypage input:focus,.layui-laypage select:focus{border-color:#009688!important}.layui-laypage button{margin-left:10px;padding:0 10px;cursor:pointer}.layui-flow-more{margin:10px 0;text-align:center;color:#999;font-size:14px}.layui-flow-more a{height:32px;line-height:32px}.layui-flow-more a *{display:inline-block;vertical-align:top}.layui-flow-more a cite{padding:0 20px;border-radius:3px;background-color:#eee;color:#333;font-style:normal}.layui-flow-more a cite:hover{opacity:.8}.layui-flow-more a i{font-size:30px;color:#737383}.layui-table{width:100%;margin:10px 0;background-color:#fff;color:#5f5f5f}.layui-table tr{transition:all .3s;-webkit-transition:all .3s}.layui-table th{text-align:left;font-weight:400}.layui-table tbody tr:hover,.layui-table thead tr,.layui-table-click,.layui-table-header,.layui-table-hover,.layui-table-mend,.layui-table-patch,.layui-table-tool,.layui-table-total,.layui-table-total tr{background-color:#fafafa}.layui-table[lay-even] tr:nth-child(even){background-color:#f2f2f2}.layui-table td,.layui-table th,.layui-table-col-set,.layui-table-fixed-r,.layui-table-grid-down,.layui-table-header,.layui-table-page,.layui-table-tips-main,.layui-table-tool,.layui-table-total,.layui-table-view,.layui-table[lay-skin=line],.layui-table[lay-skin=row]{border-width:1px;border-style:solid;border-color:#eee}.layui-table td,.layui-table th{position:relative;padding:9px 15px;min-height:20px;line-height:20px;font-size:14px}.layui-table[lay-skin=line] td,.layui-table[lay-skin=line] th{border-width:0;border-bottom-width:1px}.layui-table[lay-skin=row] td,.layui-table[lay-skin=row] th{border-width:0;border-right-width:1px}.layui-table[lay-skin=nob] td,.layui-table[lay-skin=nob] th{border:none}.layui-table img{max-width:100px}.layui-table[lay-size=lg] td,.layui-table[lay-size=lg] th{padding-top:15px;padding-right:30px;padding-bottom:15px;padding-left:30px}.layui-table-view .layui-table[lay-size=lg] .layui-table-cell{height:50px;line-height:40px}.layui-table[lay-size=sm] td,.layui-table[lay-size=sm] th{padding-top:5px;padding-right:10px;padding-bottom:5px;padding-left:10px;font-size:12px}.layui-table-view .layui-table[lay-size=sm] .layui-table-cell{height:30px;line-height:20px;padding-top:5px;padding-right:5px}.layui-table[lay-data]{display:none}.layui-table-box{position:relative;overflow:hidden}.layui-table-view{margin:10px 0}.layui-table-view .layui-table{position:relative;width:auto;margin:0;border:0;border-collapse:separate}.layui-table-view .layui-table[lay-skin=line]{border-width:0;border-right-width:1px}.layui-table-view .layui-table[lay-skin=row]{border-width:0;border-bottom-width:1px}.layui-table-view .layui-table td,.layui-table-view .layui-table th{padding:0;border-top:none;border-left:none}.layui-table-view .layui-table th.layui-unselect .layui-table-cell span{cursor:pointer}.layui-table-view .layui-table td{cursor:default}.layui-table-view .layui-table td[data-edit=text]{cursor:text}.layui-table-view .layui-form-checkbox[lay-skin=primary] i{width:18px;height:18px}.layui-table-view .layui-form-radio{line-height:0;padding:0}.layui-table-view .layui-form-radio>i{margin:0;font-size:20px}.layui-table-init{position:absolute;left:0;top:0;width:100%;height:100%;text-align:center;z-index:110}.layui-table-init .layui-icon{position:absolute;left:50%;top:50%;margin:-15px 0 0 -15px;font-size:30px;color:#c2c2c2}.layui-table-header{border-width:0;border-bottom-width:1px;overflow:hidden}.layui-table-header .layui-table{margin-bottom:-1px}.layui-table-column{position:relative;width:100%;min-height:41px;padding:8px 16px;border-width:0;border-bottom-width:1px}.layui-table-column .layui-btn-container{margin-bottom:-8px}.layui-table-column .layui-btn-container .layui-btn{margin-right:8px;margin-bottom:8px}.layui-table-tool .layui-inline[lay-event]{position:relative;width:26px;height:26px;padding:5px;line-height:16px;margin-right:10px;text-align:center;color:#333;border:1px solid #ccc;cursor:pointer;-webkit-transition:.5s all;transition:.5s all}.layui-table-tool .layui-inline[lay-event]:hover{border:1px solid #999}.layui-table-tool-temp{padding-right:120px}.layui-table-tool-self{position:absolute;right:17px;top:10px}.layui-table-tool .layui-table-tool-self .layui-inline[lay-event]{margin:0 0 0 10px}.layui-table-tool-panel{position:absolute;top:29px;left:-1px;padding:5px 0;min-width:150px;min-height:40px;border:1px solid #d2d2d2;text-align:left;overflow-y:auto;background-color:#fff;box-shadow:0 2px 4px rgba(0,0,0,.12)}.layui-table-tool-panel li{padding:0 10px;line-height:30px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-transition:.5s all;transition:.5s all}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary]{width:100%}.layui-table-tool-panel li:hover{background-color:#f6f6f6}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary]{padding-left:28px}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary] i{position:absolute;left:0;top:0}.layui-table-tool-panel li .layui-form-checkbox[lay-skin=primary] span{padding:0}.layui-table-tool .layui-table-tool-self .layui-table-tool-panel{left:auto;right:-1px}.layui-table-col-set{position:absolute;right:0;top:0;width:20px;height:100%;border-width:0;border-left-width:1px;background-color:#fff}.layui-table-sort{width:10px;height:20px;margin-left:5px;cursor:pointer!important}.layui-table-sort .layui-edge{position:absolute;left:5px;border-width:5px}.layui-table-sort .layui-table-sort-asc{top:3px;border-top:none;border-bottom-style:solid;border-bottom-color:#b2b2b2}.layui-table-sort .layui-table-sort-asc:hover{border-bottom-color:#5f5f5f}.layui-table-sort .layui-table-sort-desc{bottom:5px;border-bottom:none;border-top-style:solid;border-top-color:#b2b2b2}.layui-table-sort .layui-table-sort-desc:hover{border-top-color:#5f5f5f}.layui-table-sort[lay-sort=asc] .layui-table-sort-asc{border-bottom-color:#000}.layui-table-sort[lay-sort=desc] .layui-table-sort-desc{border-top-color:#000}.layui-table-cell{height:38px;line-height:28px;padding:6px 15px;position:relative;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;box-sizing:border-box}.layui-table-cell .layui-form-checkbox[lay-skin=primary]{top:-1px;padding:0}.layui-table-cell .layui-table-link{color:#01aaed}.layui-table-cell .layui-btn{vertical-align:inherit}.layui-table-cell[align=center]{-webkit-box-pack:center}.layui-table-cell[align=right]{-webkit-box-pack:end}.laytable-cell-checkbox,.laytable-cell-numbers,.laytable-cell-radio,.laytable-cell-space{text-align:center;-webkit-box-pack:center}.layui-table-body{position:relative;overflow:auto;margin-right:-1px;margin-bottom:-1px}.layui-table-body .layui-none{line-height:26px;padding:30px 15px;text-align:center;color:#999}.layui-table-fixed{position:absolute;left:0;top:0;z-index:101}.layui-table-fixed .layui-table-body{overflow:hidden}.layui-table-fixed-l{box-shadow:1px 0 8px rgba(0,0,0,.08)}.layui-table-fixed-r{left:auto;right:-1px;border-width:0;border-left-width:1px;box-shadow:-1px 0 8px rgba(0,0,0,.08)}.layui-table-fixed-r .layui-table-header{position:relative;overflow:visible}.layui-table-mend{position:absolute;right:-49px;top:0;height:100%;width:50px}.layui-table-tool{position:relative;z-index:890;width:100%;min-height:50px;line-height:30px;padding:10px 15px;border-width:0;border-bottom-width:1px}.layui-table-tool .layui-btn-container{margin-bottom:-10px}.layui-table-total{margin-bottom:-1px;border-width:0;border-top-width:1px;overflow:hidden}.layui-table-page{z-index:880;border-width:0;border-top-width:1px;margin-bottom:-1px;white-space:nowrap;overflow:hidden}.layui-table-page>div{height:26px}.layui-table-page .layui-laypage{margin:0}.layui-table-page .layui-laypage a,.layui-table-page .layui-laypage span{height:26px;line-height:26px;margin-bottom:10px;border:none;background:0 0}.layui-table-page .layui-laypage a,.layui-table-page .layui-laypage span.layui-laypage-curr{padding:0 12px}.layui-table-page .layui-laypage span{margin-left:0;padding:0}.layui-table-page .layui-laypage .layui-laypage-prev{margin-left:-11px!important}.layui-table-page .layui-laypage .layui-laypage-curr .layui-laypage-em{left:0;top:0;padding:0}.layui-table-page .layui-laypage button,.layui-table-page .layui-laypage input{height:26px;line-height:26px}.layui-table-page .layui-laypage input{width:40px}.layui-table-page .layui-laypage button{padding:0 10px}.layui-table-page select{height:18px}.layui-table-pagebar{float:right;line-height:23px}.layui-table-pagebar .layui-btn-sm{margin-top:-1px}.layui-table-pagebar .layui-btn-xs{margin-top:2px}.layui-table-view select[lay-ignore]{display:inline-block}.layui-table-patch .layui-table-cell{padding:0;width:30px}.layui-table-edit{position:absolute;left:0;top:0;z-index:900;min-width:100%;min-height:100%;padding:5px 14px;border-radius:0;box-shadow:1px 1px 20px rgba(0,0,0,.15);background-color:#fff}.layui-table-edit:focus{border-color:#5fb878!important}input.layui-input.layui-table-edit{height:100%}select.layui-table-edit{padding:0 0 0 10px;border-color:#d2d2d2}.layui-table-view .layui-form-checkbox,.layui-table-view .layui-form-radio,.layui-table-view .layui-form-switch{top:0;margin:0;box-sizing:content-box}.layui-table-view .layui-form-checkbox{top:-1px;height:26px;line-height:26px}.layui-table-view .layui-form-checkbox i{height:26px}.layui-table-grid .layui-table-cell{overflow:visible}.layui-table-grid-down{position:absolute;top:0;right:0;width:26px;height:100%;padding:5px 0;border-width:0;border-left-width:1px;text-align:center;background-color:#fff;color:#999;cursor:pointer}.layui-table-grid-down .layui-icon{position:absolute;top:50%;left:50%;margin:-8px 0 0 -8px}.layui-table-grid-down:hover{background-color:#fbfbfb}body .layui-table-tips .layui-layer-content{background:0 0;padding:0;box-shadow:0 1px 6px rgba(0,0,0,.12)}.layui-table-tips-main{margin:-49px 0 0 -1px;max-height:150px;padding:8px 15px;font-size:14px;overflow-y:scroll;background-color:#fff;color:#5f5f5f}.layui-table-tips-c{position:absolute;right:-3px;top:-13px;width:20px;height:20px;padding:3px;cursor:pointer;background-color:#5f5f5f;border-radius:50%;color:#fff}.layui-table-tips-c:hover{background-color:#777}.layui-table-tips-c:before{position:relative;right:-2px}.layui-upload-file{display:none!important;opacity:.01;filter:Alpha(opacity=1)}.layui-upload-list{margin:10px 0}.layui-upload-choose{max-width:200px;padding:0 10px;color:#999;font-size:14px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-upload-drag{position:relative;display:inline-block;padding:30px;border:1px dashed #e2e2e2;background-color:#fff;text-align:center;cursor:pointer;color:#999}.layui-upload-drag .layui-icon{font-size:50px;color:#009688}.layui-upload-drag[lay-over]{border-color:#009688}.layui-upload-form{display:inline-block}.layui-upload-iframe{position:absolute;width:0;height:0;border:0;visibility:hidden}.layui-upload-wrap{position:relative;display:inline-block;vertical-align:middle}.layui-upload-wrap .layui-upload-file{display:block!important;position:absolute;left:0;top:0;z-index:10;font-size:100px;width:100%;height:100%;opacity:.01;filter:Alpha(opacity=1);cursor:pointer}.layui-btn-container .layui-upload-choose{padding-left:0}.layui-menu{position:relative;margin:5px 0;background-color:#fff;box-sizing:border-box}.layui-menu *{box-sizing:border-box}.layui-menu li,.layui-menu-body-title a{padding:5px 15px}.layui-menu li{position:relative;margin:1px 0;width:calc(100% + 1px);line-height:26px;color:rgba(0,0,0,.8);font-size:14px;white-space:nowrap;cursor:pointer;transition:all .3s}.layui-menu li:hover{background-color:#f6f6f6}.layui-menu-item-parent:hover>.layui-menu-body-panel{display:block;animation-name:layui-fadein;animation-duration:.3s;animation-fill-mode:both;animation-delay:.2s}.layui-menu-item-group .layui-menu-body-title,.layui-menu-item-parent .layui-menu-body-title{padding-right:25px}.layui-menu .layui-menu-item-divider:hover,.layui-menu .layui-menu-item-group:hover,.layui-menu .layui-menu-item-none:hover{background:0 0;cursor:default}.layui-menu .layui-menu-item-group>ul{margin:5px 0 -5px}.layui-menu .layui-menu-item-group>.layui-menu-body-title{color:rgba(0,0,0,.35);user-select:none}.layui-menu .layui-menu-item-none{color:rgba(0,0,0,.35);cursor:default}.layui-menu .layui-menu-item-none{text-align:center}.layui-menu .layui-menu-item-divider{margin:5px 0;padding:0;height:0;line-height:0;border-bottom:1px solid #eee;overflow:hidden}.layui-menu .layui-menu-item-down:hover,.layui-menu .layui-menu-item-up:hover{cursor:pointer}.layui-menu .layui-menu-item-up>.layui-menu-body-title{color:rgba(0,0,0,.8)}.layui-menu .layui-menu-item-up>ul{visibility:hidden;height:0;overflow:hidden}.layui-menu .layui-menu-item-down:hover>.layui-menu-body-title>.layui-icon,.layui-menu .layui-menu-item-up>.layui-menu-body-title:hover>.layui-icon{color:#000}.layui-menu .layui-menu-item-down>ul{visibility:visible;height:auto}.layui-menu .layui-menu-item-checked,.layui-menu .layui-menu-item-checked2{background-color:#f6f6f6!important;color:#5fb878}.layui-menu .layui-menu-item-checked a,.layui-menu .layui-menu-item-checked2 a{color:#5fb878}.layui-menu .layui-menu-item-checked:after{position:absolute;right:0;top:0;bottom:0;border-right:3px solid #5fb878;content:""}.layui-menu-body-title{position:relative;overflow:hidden;text-overflow:ellipsis}.layui-menu-body-title a{display:block;margin:-5px -15px;color:rgba(0,0,0,.8)}.layui-menu-body-title a:hover{transition:all .3s}.layui-menu-body-title>.layui-icon{position:absolute;right:0;top:0;font-size:14px}.layui-menu-body-title>.layui-icon:hover{transition:all .3s}.layui-menu-body-title>.layui-icon-right{right:-1px}.layui-menu-body-panel{display:none;position:absolute;top:-7px;left:100%;z-index:1000;margin-left:13px;padding:5px 0}.layui-menu-body-panel:before{content:"";position:absolute;width:20px;left:-16px;top:0;bottom:0}.layui-menu-body-panel-left{left:auto;right:100%;margin:0 13px 0}.layui-menu-body-panel-left:before{left:auto;right:-16px}.layui-menu-lg li{line-height:32px}.layui-menu-lg .layui-menu-body-title a:hover,.layui-menu-lg li:hover{background:0 0;color:#5fb878}.layui-menu-lg li .layui-menu-body-panel{margin-left:14px}.layui-menu-lg li .layui-menu-body-panel-left{margin:0 15px 0}.layui-dropdown{position:absolute;left:-999999px;top:-999999px;z-index:77777777;margin:5px 0;min-width:100px}.layui-dropdown:before{content:"";position:absolute;width:100%;height:6px;left:0;top:-6px}.layui-nav{position:relative;padding:0 20px;background-color:#393d49;color:#fff;border-radius:2px;font-size:0;box-sizing:border-box}.layui-nav *{font-size:14px}.layui-nav .layui-nav-item{position:relative;display:inline-block;*display:inline;*zoom:1;vertical-align:middle;line-height:60px}.layui-nav .layui-nav-item a{display:block;padding:0 20px;color:#fff;color:rgba(255,255,255,.7);transition:all .3s;-webkit-transition:all .3s}.layui-nav .layui-this:after,.layui-nav-bar{content:"";position:absolute;left:0;top:0;width:0;height:5px;background-color:#5fb878;transition:all .2s;-webkit-transition:all .2s;pointer-events:none}.layui-nav-bar{z-index:1000}.layui-nav[lay-bar=disabled] .layui-nav-bar{display:none}.layui-nav .layui-nav-item a:hover,.layui-nav .layui-this a{color:#fff}.layui-nav .layui-this:after{top:auto;bottom:0;width:100%}.layui-nav-img{width:30px;height:30px;margin-right:10px;border-radius:50%}.layui-nav .layui-nav-more{position:absolute;top:0;right:3px;left:auto!important;margin-top:0;font-size:12px;cursor:pointer;transition:all .2s;-webkit-transition:all .2s}.layui-nav .layui-nav-mored,.layui-nav-itemed>a .layui-nav-more{transform:rotate(180deg)}.layui-nav-child{display:none;position:absolute;left:0;top:65px;min-width:100%;line-height:36px;padding:5px 0;box-shadow:0 2px 4px rgba(0,0,0,.12);border:1px solid #eee;background-color:#fff;z-index:100;border-radius:2px;white-space:nowrap}.layui-nav .layui-nav-child a{color:#5f5f5f;color:rgba(0,0,0,.8)}.layui-nav .layui-nav-child a:hover{background-color:#f6f6f6;color:rgba(0,0,0,.8)}.layui-nav-child dd{margin:1px 0;position:relative}.layui-nav-child dd.layui-this{background-color:#f6f6f6;color:#000}.layui-nav-child dd.layui-this:after{display:none}.layui-nav-child-r{left:auto;right:0}.layui-nav-child-c{text-align:center}.layui-nav-tree{width:200px;padding:0}.layui-nav-tree .layui-nav-item{display:block;width:100%;line-height:40px}.layui-nav-tree .layui-nav-item a{position:relative;height:40px;line-height:40px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-nav-tree .layui-nav-item>a{padding-top:5px;padding-bottom:5px}.layui-nav-tree .layui-nav-more{right:15px}.layui-nav-tree .layui-nav-item>a .layui-nav-more{padding:5px 0}.layui-nav-tree .layui-nav-bar{width:5px;height:0}.layui-side .layui-nav-tree .layui-nav-bar{width:2px}.layui-nav-tree .layui-nav-child dd.layui-this,.layui-nav-tree .layui-nav-child dd.layui-this a,.layui-nav-tree .layui-this,.layui-nav-tree .layui-this>a,.layui-nav-tree .layui-this>a:hover{background-color:#009688;color:#fff}.layui-nav-tree .layui-this:after{display:none}.layui-nav-itemed>a,.layui-nav-tree .layui-nav-title a,.layui-nav-tree .layui-nav-title a:hover{color:#fff!important}.layui-nav-tree .layui-nav-bar{background-color:#009688}.layui-nav-tree .layui-nav-child{position:relative;z-index:0;top:0;border:none;box-shadow:none}.layui-nav-tree .layui-nav-child dd{margin:0}.layui-nav-tree .layui-nav-child a{color:#fff;color:rgba(255,255,255,.7)}.layui-nav-tree .layui-nav-child,.layui-nav-tree .layui-nav-child a:hover{background:0 0;color:#fff}.layui-nav-itemed>.layui-nav-child{display:block;background-color:rgba(0,0,0,.3)!important}.layui-nav-itemed>.layui-nav-child>.layui-this>.layui-nav-child{display:block}.layui-nav-side{position:fixed;top:0;bottom:0;left:0;overflow-x:hidden;z-index:999}.layui-breadcrumb{visibility:hidden;font-size:0}.layui-breadcrumb>*{font-size:14px}.layui-breadcrumb a{color:#999!important}.layui-breadcrumb a:hover{color:#5fb878!important}.layui-breadcrumb a cite{color:#5f5f5f;font-style:normal}.layui-breadcrumb span[lay-separator]{margin:0 10px;color:#999}.layui-tab{margin:10px 0;text-align:left!important}.layui-tab[overflow]>.layui-tab-title{overflow:hidden}.layui-tab-title{position:relative;left:0;height:40px;white-space:nowrap;font-size:0;border-bottom-width:1px;border-bottom-style:solid;transition:all .2s;-webkit-transition:all .2s}.layui-tab-title li{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;font-size:14px;transition:all .2s;-webkit-transition:all .2s}.layui-tab-title li{position:relative;line-height:40px;min-width:65px;padding:0 15px;text-align:center;cursor:pointer}.layui-tab-title li a{display:block;padding:0 15px;margin:0 -15px}.layui-tab-title .layui-this{color:#000}.layui-tab-title .layui-this:after{position:absolute;left:0;top:0;content:"";width:100%;height:41px;border-width:1px;border-style:solid;border-bottom-color:#fff;border-radius:2px 2px 0 0;box-sizing:border-box;pointer-events:none}.layui-tab-bar{position:absolute;right:0;top:0;z-index:10;width:30px;height:39px;line-height:39px;border-width:1px;border-style:solid;border-radius:2px;text-align:center;background-color:#fff;cursor:pointer}.layui-tab-bar .layui-icon{position:relative;display:inline-block;top:3px;transition:all .3s;-webkit-transition:all .3s}.layui-tab-item{display:none}.layui-tab-more{padding-right:30px;height:auto!important;white-space:normal!important}.layui-tab-more li.layui-this:after{border-bottom-color:#eee;border-radius:2px}.layui-tab-more .layui-tab-bar .layui-icon{top:-2px;top:3px\0;-webkit-transform:rotate(180deg);transform:rotate(180deg)}:root .layui-tab-more .layui-tab-bar .layui-icon{top:-2px\0/IE9}.layui-tab-content{padding:15px 0}.layui-tab-title li .layui-tab-close{position:relative;display:inline-block;width:18px;height:18px;line-height:20px;margin-left:8px;top:1px;text-align:center;font-size:14px;color:#c2c2c2;transition:all .2s;-webkit-transition:all .2s}.layui-tab-title li .layui-tab-close:hover{border-radius:2px;background-color:#ff5722;color:#fff}.layui-tab-brief>.layui-tab-title .layui-this{color:#009688}.layui-tab-brief>.layui-tab-more li.layui-this:after,.layui-tab-brief>.layui-tab-title .layui-this:after{border:none;border-radius:0;border-bottom:2px solid #5fb878}.layui-tab-brief[overflow]>.layui-tab-title .layui-this:after{top:-1px}.layui-tab-card{border-width:1px;border-style:solid;border-radius:2px;box-shadow:0 2px 5px 0 rgba(0,0,0,.1)}.layui-tab-card>.layui-tab-title{background-color:#fafafa}.layui-tab-card>.layui-tab-title li{margin-right:-1px;margin-left:-1px}.layui-tab-card>.layui-tab-title .layui-this{background-color:#fff}.layui-tab-card>.layui-tab-title .layui-this:after{border-top:none;border-width:1px;border-bottom-color:#fff}.layui-tab-card>.layui-tab-title .layui-tab-bar{height:40px;line-height:40px;border-radius:0;border-top:none;border-right:none}.layui-tab-card>.layui-tab-more .layui-this{background:0 0;color:#5fb878}.layui-tab-card>.layui-tab-more .layui-this:after{border:none}.layui-timeline{padding-left:5px}.layui-timeline-item{position:relative;padding-bottom:20px}.layui-timeline-axis{position:absolute;left:-5px;top:0;z-index:10;width:20px;height:20px;line-height:20px;background-color:#fff;color:#5fb878;border-radius:50%;text-align:center;cursor:pointer}.layui-timeline-axis:hover{color:#ff5722}.layui-timeline-item:before{content:"";position:absolute;left:5px;top:0;z-index:0;width:1px;height:100%}.layui-timeline-item:first-child:before{display:block}.layui-timeline-item:last-child:before{display:none}.layui-timeline-content{padding-left:25px}.layui-timeline-title{position:relative;margin-bottom:10px;line-height:22px}.layui-badge,.layui-badge-dot,.layui-badge-rim{position:relative;display:inline-block;padding:0 6px;font-size:12px;text-align:center;background-color:#ff5722;color:#fff;border-radius:2px}.layui-badge{height:18px;line-height:18px}.layui-badge-dot{width:8px;height:8px;padding:0;border-radius:50%}.layui-badge-rim{height:18px;line-height:18px;border-width:1px;border-style:solid;background-color:#fff;color:#5f5f5f}.layui-btn .layui-badge,.layui-btn .layui-badge-dot{margin-left:5px}.layui-nav .layui-badge,.layui-nav .layui-badge-dot{position:absolute;top:50%;margin:-5px 6px 0}.layui-nav .layui-badge{margin-top:-10px}.layui-tab-title .layui-badge,.layui-tab-title .layui-badge-dot{left:5px;top:-2px}.layui-carousel{position:relative;left:0;top:0;background-color:#f8f8f8}.layui-carousel>[carousel-item]{position:relative;width:100%;height:100%;overflow:hidden}.layui-carousel>[carousel-item]:before{position:absolute;content:'\e63d';left:50%;top:50%;width:100px;line-height:20px;margin:-10px 0 0 -50px;text-align:center;color:#c2c2c2;font-family:layui-icon!important;font-size:30px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.layui-carousel>[carousel-item]>*{display:none;position:absolute;left:0;top:0;width:100%;height:100%;background-color:#f8f8f8;transition-duration:.3s;-webkit-transition-duration:.3s}.layui-carousel-updown>*{-webkit-transition:.3s ease-in-out up;transition:.3s ease-in-out up}.layui-carousel-arrow{display:none\0;opacity:0;position:absolute;left:10px;top:50%;margin-top:-18px;width:36px;height:36px;line-height:36px;text-align:center;font-size:20px;border:none 0;border-radius:50%;background-color:rgba(0,0,0,.2);color:#fff;-webkit-transition-duration:.3s;transition-duration:.3s;cursor:pointer}.layui-carousel-arrow[lay-type=add]{left:auto!important;right:10px}.layui-carousel[lay-arrow=always] .layui-carousel-arrow{opacity:1;left:20px}.layui-carousel[lay-arrow=always] .layui-carousel-arrow[lay-type=add]{right:20px}.layui-carousel[lay-arrow=none] .layui-carousel-arrow{display:none}.layui-carousel-arrow:hover,.layui-carousel-ind ul:hover{background-color:rgba(0,0,0,.35)}.layui-carousel:hover .layui-carousel-arrow{display:block\0;opacity:1;left:20px}.layui-carousel:hover .layui-carousel-arrow[lay-type=add]{right:20px}.layui-carousel-ind{position:relative;top:-35px;width:100%;line-height:0!important;text-align:center;font-size:0}.layui-carousel[lay-indicator=outside]{margin-bottom:30px}.layui-carousel[lay-indicator=outside] .layui-carousel-ind{top:10px}.layui-carousel[lay-indicator=outside] .layui-carousel-ind ul{background-color:rgba(0,0,0,.5)}.layui-carousel[lay-indicator=none] .layui-carousel-ind{display:none}.layui-carousel-ind ul{display:inline-block;padding:5px;background-color:rgba(0,0,0,.2);border-radius:10px;-webkit-transition-duration:.3s;transition-duration:.3s}.layui-carousel-ind li{display:inline-block;width:10px;height:10px;margin:0 3px;font-size:14px;background-color:#eee;background-color:rgba(255,255,255,.5);border-radius:50%;cursor:pointer;-webkit-transition-duration:.3s;transition-duration:.3s}.layui-carousel-ind li:hover{background-color:rgba(255,255,255,.7)}.layui-carousel-ind li.layui-this{background-color:#fff}.layui-carousel>[carousel-item]>.layui-carousel-next,.layui-carousel>[carousel-item]>.layui-carousel-prev,.layui-carousel>[carousel-item]>.layui-this{display:block}.layui-carousel>[carousel-item]>.layui-this{left:0}.layui-carousel>[carousel-item]>.layui-carousel-prev{left:-100%}.layui-carousel>[carousel-item]>.layui-carousel-next{left:100%}.layui-carousel>[carousel-item]>.layui-carousel-next.layui-carousel-left,.layui-carousel>[carousel-item]>.layui-carousel-prev.layui-carousel-right{left:0}.layui-carousel>[carousel-item]>.layui-this.layui-carousel-left{left:-100%}.layui-carousel>[carousel-item]>.layui-this.layui-carousel-right{left:100%}.layui-carousel[lay-anim=updown] .layui-carousel-arrow{left:50%!important;top:20px;margin:0 0 0 -18px}.layui-carousel[lay-anim=updown] .layui-carousel-arrow[lay-type=add]{top:auto!important;bottom:20px}.layui-carousel[lay-anim=updown] .layui-carousel-ind{position:absolute;top:50%;right:20px;width:auto;height:auto}.layui-carousel[lay-anim=updown] .layui-carousel-ind ul{padding:3px 5px}.layui-carousel[lay-anim=updown] .layui-carousel-ind li{display:block;margin:6px 0}.layui-carousel[lay-anim=updown]>[carousel-item]>*{left:0!important}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-this{top:0}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-prev{top:-100%}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-next{top:100%}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-next.layui-carousel-left,.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-carousel-prev.layui-carousel-right{top:0}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-this.layui-carousel-left{top:-100%}.layui-carousel[lay-anim=updown]>[carousel-item]>.layui-this.layui-carousel-right{top:100%}.layui-carousel[lay-anim=fade]>[carousel-item]>*{left:0!important}.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-next,.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-prev{opacity:0}.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-next.layui-carousel-left,.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-carousel-prev.layui-carousel-right{opacity:1}.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-this.layui-carousel-left,.layui-carousel[lay-anim=fade]>[carousel-item]>.layui-this.layui-carousel-right{opacity:0}.layui-fixbar{position:fixed;right:15px;bottom:15px;z-index:999999}.layui-fixbar li{width:50px;height:50px;line-height:50px;margin-bottom:1px;text-align:center;cursor:pointer;font-size:30px;background-color:#9f9f9f;color:#fff;border-radius:2px;opacity:.95}.layui-fixbar li:hover{opacity:.85}.layui-fixbar li:active{opacity:1}.layui-fixbar .layui-fixbar-top{display:none;font-size:40px}body .layui-util-face{border:none;background:0 0}body .layui-util-face .layui-layer-content{padding:0;background-color:#fff;color:#5f5f5f;box-shadow:none}.layui-util-face .layui-layer-TipsG{display:none}.layui-util-face ul{position:relative;width:372px;padding:10px;border:1px solid #d9d9d9;background-color:#fff;box-shadow:0 0 20px rgba(0,0,0,.2)}.layui-util-face ul li{cursor:pointer;float:left;border:1px solid #e8e8e8;height:22px;width:26px;overflow:hidden;margin:-1px 0 0 -1px;padding:4px 2px;text-align:center}.layui-util-face ul li:hover{position:relative;z-index:2;border:1px solid #eb7350;background:#fff9ec}.layui-code{position:relative;margin:10px 0;padding:15px;line-height:20px;border:1px solid #eee;border-left-width:6px;background-color:#fafafa;color:#333;font-family:Courier New;font-size:12px}.layui-transfer-box,.layui-transfer-header,.layui-transfer-search{border-width:0;border-style:solid;border-color:#eee}.layui-transfer-box{position:relative;display:inline-block;vertical-align:middle;border-width:1px;width:200px;height:360px;border-radius:2px;background-color:#fff}.layui-transfer-box .layui-form-checkbox{width:100%;margin:0!important}.layui-transfer-header{height:38px;line-height:38px;padding:0 10px;border-bottom-width:1px}.layui-transfer-search{position:relative;padding:10px;border-bottom-width:1px}.layui-transfer-search .layui-input{height:32px;padding-left:30px;font-size:12px}.layui-transfer-search .layui-icon-search{position:absolute;left:20px;top:50%;margin-top:-8px;color:#5f5f5f}.layui-transfer-active{margin:0 15px;display:inline-block;vertical-align:middle}.layui-transfer-active .layui-btn{display:block;margin:0;padding:0 15px;background-color:#5fb878;border-color:#5fb878;color:#fff}.layui-transfer-active .layui-btn-disabled{background-color:#fbfbfb;border-color:#eee;color:#d2d2d2}.layui-transfer-active .layui-btn:first-child{margin-bottom:15px}.layui-transfer-active .layui-btn .layui-icon{margin:0;font-size:14px!important}.layui-transfer-data{padding:5px 0;overflow:auto}.layui-transfer-data li{height:32px;line-height:32px;padding:0 10px}.layui-transfer-data li:hover{background-color:#f6f6f6;transition:.5s all}.layui-transfer-data .layui-none{padding:15px 10px;text-align:center;color:#999}.layui-rate,.layui-rate *{display:inline-block;vertical-align:middle}.layui-rate{padding:10px 5px 10px 0;font-size:0}.layui-rate li i.layui-icon{font-size:20px;color:#ffb800}.layui-rate li i.layui-icon{margin-right:5px;transition:all .3s;-webkit-transition:all .3s}.layui-rate li i:hover{cursor:pointer;transform:scale(1.12);-webkit-transform:scale(1.12)}.layui-rate[readonly] li i:hover{cursor:default;transform:scale(1)}.layui-colorpicker{width:26px;height:26px;border:1px solid #eee;padding:5px;border-radius:2px;line-height:24px;display:inline-block;cursor:pointer;transition:all .3s;-webkit-transition:all .3s}.layui-colorpicker:hover{border-color:#d2d2d2}.layui-colorpicker.layui-colorpicker-lg{width:34px;height:34px;line-height:32px}.layui-colorpicker.layui-colorpicker-sm{width:24px;height:24px;line-height:22px}.layui-colorpicker.layui-colorpicker-xs{width:22px;height:22px;line-height:20px}.layui-colorpicker-trigger-bgcolor{display:block;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);border-radius:2px}.layui-colorpicker-trigger-span{display:block;height:100%;box-sizing:border-box;border:1px solid rgba(0,0,0,.15);border-radius:2px;text-align:center}.layui-colorpicker-trigger-i{display:inline-block;color:#fff;font-size:12px}.layui-colorpicker-trigger-i.layui-icon-close{color:#999}.layui-colorpicker-main{position:absolute;left:-999999px;top:-999999px;z-index:77777777;width:280px;margin:5px 0;padding:7px;background:#fff;border:1px solid #d2d2d2;border-radius:2px;box-shadow:0 2px 4px rgba(0,0,0,.12)}.layui-colorpicker-main-wrapper{height:180px;position:relative}.layui-colorpicker-basis{width:260px;height:100%;position:relative}.layui-colorpicker-basis-white{width:100%;height:100%;position:absolute;top:0;left:0;background:linear-gradient(90deg,#fff,hsla(0,0%,100%,0))}.layui-colorpicker-basis-black{width:100%;height:100%;position:absolute;top:0;left:0;background:linear-gradient(0deg,#000,transparent)}.layui-colorpicker-basis-cursor{width:10px;height:10px;border:1px solid #fff;border-radius:50%;position:absolute;top:-3px;right:-3px;cursor:pointer}.layui-colorpicker-side{position:absolute;top:0;right:0;width:12px;height:100%;background:linear-gradient(red,#ff0,#0f0,#0ff,#00f,#f0f,red)}.layui-colorpicker-side-slider{width:100%;height:5px;box-shadow:0 0 1px #888;box-sizing:border-box;background:#fff;border-radius:1px;border:1px solid #f0f0f0;cursor:pointer;position:absolute;left:0}.layui-colorpicker-main-alpha{display:none;height:12px;margin-top:7px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.layui-colorpicker-alpha-bgcolor{height:100%;position:relative}.layui-colorpicker-alpha-slider{width:5px;height:100%;box-shadow:0 0 1px #888;box-sizing:border-box;background:#fff;border-radius:1px;border:1px solid #f0f0f0;cursor:pointer;position:absolute;top:0}.layui-colorpicker-main-pre{padding-top:7px;font-size:0}.layui-colorpicker-pre{width:20px;height:20px;border-radius:2px;display:inline-block;margin-left:6px;margin-bottom:7px;cursor:pointer}.layui-colorpicker-pre:nth-child(11n+1){margin-left:0}.layui-colorpicker-pre-isalpha{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.layui-colorpicker-pre.layui-this{box-shadow:0 0 3px 2px rgba(0,0,0,.15)}.layui-colorpicker-pre>div{height:100%;border-radius:2px}.layui-colorpicker-main-input{text-align:right;padding-top:7px}.layui-colorpicker-main-input .layui-btn-container .layui-btn{margin:0 0 0 10px}.layui-colorpicker-main-input div.layui-inline{float:left;margin-right:10px;font-size:14px}.layui-colorpicker-main-input input.layui-input{width:150px;height:30px;color:#5f5f5f}.layui-slider{height:4px;background:#eee;border-radius:3px;position:relative;cursor:pointer}.layui-slider-bar{border-radius:3px;position:absolute;height:100%}.layui-slider-step{position:absolute;top:0;width:4px;height:4px;border-radius:50%;background:#fff;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.layui-slider-wrap{width:36px;height:36px;position:absolute;top:-16px;-webkit-transform:translateX(-50%);transform:translateX(-50%);z-index:10;text-align:center}.layui-slider-wrap-btn{width:12px;height:12px;border-radius:50%;background:#fff;display:inline-block;vertical-align:middle;cursor:pointer;transition:.3s}.layui-slider-wrap:after{content:"";height:100%;display:inline-block;vertical-align:middle}.layui-slider-wrap-btn.layui-slider-hover,.layui-slider-wrap-btn:hover{transform:scale(1.2)}.layui-slider-wrap-btn.layui-disabled:hover{transform:scale(1)!important}.layui-slider-tips{position:absolute;top:-42px;z-index:77777777;white-space:nowrap;display:none;-webkit-transform:translateX(-50%);transform:translateX(-50%);color:#fff;background:#000;border-radius:3px;height:25px;line-height:25px;padding:0 10px}.layui-slider-tips:after{content:"";position:absolute;bottom:-12px;left:50%;margin-left:-6px;width:0;height:0;border-width:6px;border-style:solid;border-color:#000 transparent transparent transparent}.layui-slider-input{width:70px;height:32px;border:1px solid #eee;border-radius:3px;font-size:16px;line-height:32px;position:absolute;right:0;top:-14px}.layui-slider-input-btn{position:absolute;top:0;right:0;width:20px;height:100%;border-left:1px solid #eee}.layui-slider-input-btn i{cursor:pointer;position:absolute;right:0;bottom:0;width:20px;height:50%;font-size:12px;line-height:16px;text-align:center;color:#999}.layui-slider-input-btn i:first-child{top:0;border-bottom:1px solid #eee}.layui-slider-input-txt{height:100%;font-size:14px}.layui-slider-input-txt input{height:100%;border:none}.layui-slider-input-btn i:hover{color:#009688}.layui-slider-vertical{width:4px;margin-left:33px}.layui-slider-vertical .layui-slider-bar{width:4px}.layui-slider-vertical .layui-slider-step{top:auto;left:0;-webkit-transform:translateY(50%);transform:translateY(50%)}.layui-slider-vertical .layui-slider-wrap{top:auto;left:-16px;-webkit-transform:translateY(50%);transform:translateY(50%)}.layui-slider-vertical .layui-slider-tips{top:auto;left:2px}@media \0screen{.layui-slider-wrap-btn{margin-left:-20px}.layui-slider-vertical .layui-slider-wrap-btn{margin-left:0;margin-bottom:-20px}.layui-slider-vertical .layui-slider-tips{margin-left:-8px}.layui-slider>span{margin-left:8px}}.layui-tree{line-height:22px}.layui-tree .layui-form-checkbox{margin:0!important}.layui-tree-set{width:100%;position:relative}.layui-tree-pack{display:none;padding-left:20px;position:relative}.layui-tree-line .layui-tree-pack{padding-left:27px}.layui-tree-line .layui-tree-set .layui-tree-set:after{content:"";position:absolute;top:14px;left:-9px;width:17px;height:0;border-top:1px dotted #c0c4cc}.layui-tree-entry{position:relative;padding:3px 0;height:20px;white-space:nowrap}.layui-tree-entry:hover{background-color:#eee}.layui-tree-line .layui-tree-entry:hover{background-color:rgba(0,0,0,0)}.layui-tree-line .layui-tree-entry:hover .layui-tree-txt{color:#999;text-decoration:underline;transition:.3s}.layui-tree-main{display:inline-block;vertical-align:middle;cursor:pointer;padding-right:10px}.layui-tree-line .layui-tree-set:before{content:"";position:absolute;top:0;left:-9px;width:0;height:100%;border-left:1px dotted #c0c4cc}.layui-tree-line .layui-tree-set.layui-tree-setLineShort:before{height:13px}.layui-tree-line .layui-tree-set.layui-tree-setHide:before{height:0}.layui-tree-iconClick{display:inline-block;vertical-align:middle;position:relative;height:20px;line-height:20px;margin:0 10px;color:#c0c4cc}.layui-tree-icon{height:12px;line-height:12px;width:12px;text-align:center;border:1px solid #c0c4cc}.layui-tree-iconClick .layui-icon{font-size:18px}.layui-tree-icon .layui-icon{font-size:12px;color:#5f5f5f}.layui-tree-iconArrow{padding:0 5px}.layui-tree-iconArrow:after{content:"";position:absolute;left:4px;top:3px;z-index:100;width:0;height:0;border-width:5px;border-style:solid;border-color:transparent transparent transparent #c0c4cc;transition:.5s}.layui-tree-spread>.layui-tree-entry .layui-tree-iconClick>.layui-tree-iconArrow:after{transform:rotate(90deg) translate(3px,4px)}.layui-tree-txt{display:inline-block;vertical-align:middle;color:#555}.layui-tree-search{margin-bottom:15px;color:#5f5f5f}.layui-tree-btnGroup{visibility:hidden;display:inline-block;vertical-align:middle;position:relative}.layui-tree-btnGroup .layui-icon{display:inline-block;vertical-align:middle;padding:0 2px;cursor:pointer}.layui-tree-btnGroup .layui-icon:hover{color:#999;transition:.3s}.layui-tree-entry:hover .layui-tree-btnGroup{visibility:visible}.layui-tree-editInput{position:relative;display:inline-block;vertical-align:middle;height:20px;line-height:20px;padding:0 3px;border:none;background-color:rgba(0,0,0,.05)}.layui-tree-emptyText{text-align:center;color:#999}.layui-anim{-webkit-animation-duration:.3s;-webkit-animation-fill-mode:both;animation-duration:.3s;animation-fill-mode:both}.layui-anim.layui-icon{display:inline-block}.layui-anim-loop{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.layui-trans,.layui-trans a{transition:all .2s;-webkit-transition:all .2s}@-webkit-keyframes layui-rotate{from{-webkit-transform:rotate(0)}to{-webkit-transform:rotate(360deg)}}@keyframes layui-rotate{from{transform:rotate(0)}to{transform:rotate(360deg)}}.layui-anim-rotate{-webkit-animation-name:layui-rotate;animation-name:layui-rotate;-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-timing-function:linear;animation-timing-function:linear}@-webkit-keyframes layui-up{from{-webkit-transform:translate3d(0,100%,0);opacity:.3}to{-webkit-transform:translate3d(0,0,0);opacity:1}}@keyframes layui-up{from{transform:translate3d(0,100%,0);opacity:.3}to{transform:translate3d(0,0,0);opacity:1}}.layui-anim-up{-webkit-animation-name:layui-up;animation-name:layui-up}@-webkit-keyframes layui-upbit{from{-webkit-transform:translate3d(0,15px,0);opacity:.3}to{-webkit-transform:translate3d(0,0,0);opacity:1}}@keyframes layui-upbit{from{transform:translate3d(0,15px,0);opacity:.3}to{transform:translate3d(0,0,0);opacity:1}}.layui-anim-upbit{-webkit-animation-name:layui-upbit;animation-name:layui-upbit}@keyframes layui-down{0%{opacity:.3;transform:translate3d(0,-100%,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-anim-down{animation-name:layui-down}@keyframes layui-downbit{0%{opacity:.3;transform:translate3d(0,-5px,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-anim-downbit{animation-name:layui-downbit}@-webkit-keyframes layui-scale{0%{opacity:.3;-webkit-transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1)}}@keyframes layui-scale{0%{opacity:.3;-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-ms-transform:scale(1);transform:scale(1)}}.layui-anim-scale{-webkit-animation-name:layui-scale;animation-name:layui-scale}@-webkit-keyframes layui-scale-spring{0%{opacity:.5;-webkit-transform:scale(.5)}80%{opacity:.8;-webkit-transform:scale(1.1)}100%{opacity:1;-webkit-transform:scale(1)}}@keyframes layui-scale-spring{0%{opacity:.5;transform:scale(.5)}80%{opacity:.8;transform:scale(1.1)}100%{opacity:1;transform:scale(1)}}.layui-anim-scaleSpring{-webkit-animation-name:layui-scale-spring;animation-name:layui-scale-spring}@keyframes layui-scalesmall{0%{opacity:.3;transform:scale(1.5)}100%{opacity:1;transform:scale(1)}}.layui-anim-scalesmall{animation-name:layui-scalesmall}@keyframes layui-scalesmall-spring{0%{opacity:.3;transform:scale(1.5)}80%{opacity:.8;transform:scale(.9)}100%{opacity:1;transform:scale(1)}}.layui-anim-scalesmall-spring{animation-name:layui-scalesmall-spring}@-webkit-keyframes layui-fadein{0%{opacity:0}100%{opacity:1}}@keyframes layui-fadein{0%{opacity:0}100%{opacity:1}}.layui-anim-fadein{-webkit-animation-name:layui-fadein;animation-name:layui-fadein}@-webkit-keyframes layui-fadeout{0%{opacity:1}100%{opacity:0}}@keyframes layui-fadeout{0%{opacity:1}100%{opacity:0}}.layui-anim-fadeout{-webkit-animation-name:layui-fadeout;animation-name:layui-fadeout} diff --git a/module/static/layui/css/modules/code.css b/module/static/layui/css/modules/code.css new file mode 100644 index 0000000..f7c9d01 --- /dev/null +++ b/module/static/layui/css/modules/code.css @@ -0,0 +1 @@ +html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-view{display:block;position:relative;margin:10px 0;padding:0;border:1px solid #eee;border-left-width:6px;background-color:#fafafa;color:#333;font-family:Courier New;font-size:13px}.layui-code-title{position:relative;padding:0 10px;height:40px;line-height:40px;border-bottom:1px solid #eee;font-size:12px}.layui-code-title>.layui-code-about{position:absolute;right:10px;top:0;color:#b7b7b7}.layui-code-about>a{padding-left:10px}.layui-code-view>.layui-code-ol,.layui-code-view>.layui-code-ul{position:relative;overflow:auto}.layui-code-view>.layui-code-ol>li{position:relative;margin-left:45px;line-height:20px;padding:0 10px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view>.layui-code-ol>li:first-child,.layui-code-view>.layui-code-ul>li:first-child{padding-top:10px}.layui-code-view>.layui-code-ol>li:last-child,.layui-code-view>.layui-code-ul>li:last-child{padding-bottom:10px}.layui-code-view>.layui-code-ul>li{position:relative;line-height:20px;padding:0 10px;list-style-type:none;*list-style-type:none;background-color:#fff}.layui-code-view pre{margin:0}.layui-code-dark{border:1px solid #0c0c0c;border-left-color:#3f3f3f;background-color:#0c0c0c;color:#c2be9e}.layui-code-dark>.layui-code-title{border-bottom:none}.layui-code-dark>.layui-code-ol>li,.layui-code-dark>.layui-code-ul>li{background-color:#3f3f3f;border-left:none}.layui-code-dark>.layui-code-ul>li{margin-left:6px}.layui-code-demo .layui-code{visibility:visible!important;margin:-15px;border-top:none;border-right:none;border-bottom:none}.layui-code-demo .layui-tab-content{padding:15px;border-top:none} diff --git a/module/static/layui/css/modules/laydate/default/laydate.css b/module/static/layui/css/modules/laydate/default/laydate.css new file mode 100644 index 0000000..da8acbd --- /dev/null +++ b/module/static/layui/css/modules/laydate/default/laydate.css @@ -0,0 +1 @@ +html #layuicss-laydate{display:none;position:absolute;width:1989px}.layui-laydate *{margin:0;padding:0}.layui-laydate,.layui-laydate *{box-sizing:border-box}.layui-laydate{position:absolute;z-index:66666666;margin:5px 0;border-radius:2px;font-size:14px;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.layui-laydate-main{width:272px}.layui-laydate-content td,.layui-laydate-header *,.layui-laydate-list li{transition-duration:.3s;-webkit-transition-duration:.3s}@keyframes laydate-downbit{0%{opacity:.3;transform:translate3d(0,-5px,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-laydate{animation-name:laydate-downbit}.layui-laydate-static{position:relative;z-index:0;display:inline-block;margin:0;-webkit-animation:none;animation:none}.laydate-ym-show .laydate-next-m,.laydate-ym-show .laydate-prev-m{display:none!important}.laydate-ym-show .laydate-next-y,.laydate-ym-show .laydate-prev-y{display:inline-block!important}.laydate-ym-show .laydate-set-ym span[lay-type=month]{display:none!important}.laydate-time-show .laydate-set-ym span[lay-type=month],.laydate-time-show .laydate-set-ym span[lay-type=year],.laydate-time-show .layui-laydate-header .layui-icon{display:none!important}.layui-laydate-header{position:relative;line-height:30px;padding:10px 70px 5px}.layui-laydate-header *{display:inline-block;vertical-align:bottom}.layui-laydate-header i{position:absolute;top:10px;padding:0 5px;color:#999;font-size:18px;cursor:pointer}.layui-laydate-header i.laydate-prev-y{left:15px}.layui-laydate-header i.laydate-prev-m{left:45px}.layui-laydate-header i.laydate-next-y{right:15px}.layui-laydate-header i.laydate-next-m{right:45px}.laydate-set-ym{width:100%;text-align:center;box-sizing:border-box;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.laydate-set-ym span{padding:0 10px;cursor:pointer}.laydate-time-text{cursor:default!important}.layui-laydate-content{position:relative;padding:10px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-laydate-content table{border-collapse:collapse;border-spacing:0}.layui-laydate-content td,.layui-laydate-content th{width:36px;height:30px;padding:5px;text-align:center}.layui-laydate-content th{font-weight:400}.layui-laydate-content td{position:relative;cursor:pointer}.laydate-day-mark{position:absolute;left:0;top:0;width:100%;line-height:30px;font-size:12px;overflow:hidden}.laydate-day-mark::after{position:absolute;content:'';right:2px;top:2px;width:5px;height:5px;border-radius:50%}.laydate-day-holidays:before{position:absolute;left:0;top:0;font-size:12px;transform:scale(.7)}.laydate-day-holidays:before{content:'\4F11';color:#ff5722}.laydate-day-holidays[type=work]:before{content:'\73ED';color:inherit}.layui-laydate .layui-this .laydate-day-holidays:before{color:#fff}.layui-laydate-footer{position:relative;height:46px;line-height:26px;padding:10px}.layui-laydate-footer span{display:inline-block;vertical-align:top;height:26px;line-height:24px;padding:0 10px;border:1px solid #c9c9c9;border-radius:2px;background-color:#fff;font-size:12px;cursor:pointer;white-space:nowrap;transition:all .3s}.layui-laydate-footer span:hover{color:#5fb878}.layui-laydate-footer span.layui-laydate-preview{cursor:default;border-color:transparent!important}.layui-laydate-footer span.layui-laydate-preview:hover{color:#666}.layui-laydate-footer span:first-child.layui-laydate-preview{padding-left:0}.laydate-footer-btns{position:absolute;right:10px;top:10px}.laydate-footer-btns span{margin:0 0 0 -1px}.layui-laydate-list{position:absolute;left:0;top:0;width:100%;height:100%;padding:10px;box-sizing:border-box;background-color:#fff}.layui-laydate-list>li{position:relative;display:inline-block;width:33.3%;height:36px;line-height:36px;margin:3px 0;vertical-align:middle;text-align:center;cursor:pointer}.laydate-month-list>li{width:25%;margin:17px 0}.laydate-time-list>li{height:100%;margin:0;line-height:normal;cursor:default}.laydate-time-list p{position:relative;top:-4px;line-height:29px}.laydate-time-list ol{height:181px;overflow:hidden}.laydate-time-list>li:hover ol{overflow-y:auto}.laydate-time-list ol li{width:130%;padding-left:33px;height:30px;line-height:30px;text-align:left;cursor:pointer}.layui-laydate-hint{position:absolute;top:115px;left:50%;width:250px;margin-left:-125px;line-height:20px;padding:15px;text-align:center;font-size:12px;color:#ff5722}.layui-laydate-range{width:546px}.layui-laydate-range .layui-laydate-main{display:inline-block;vertical-align:middle}.layui-laydate-range .laydate-main-list-1 .layui-laydate-content,.layui-laydate-range .laydate-main-list-1 .layui-laydate-header{border-left:1px solid #e2e2e2}.layui-laydate,.layui-laydate-hint{border:1px solid #d2d2d2;box-shadow:0 2px 4px rgba(0,0,0,.12);background-color:#fff;color:#666}.layui-laydate-header{border-bottom:1px solid #e2e2e2}.layui-laydate-header i:hover,.layui-laydate-header span:hover{color:#5fb878}.layui-laydate-content{border-top:none 0;border-bottom:none 0}.layui-laydate-content th{color:#333}.layui-laydate-content td{color:#666}.layui-laydate-content td.laydate-selected{background-color:#b5fff8}.laydate-selected:hover{background-color:#00f7de!important}.layui-laydate-content td:hover,.layui-laydate-list li:hover{background-color:#eee;color:#333}.laydate-time-list li ol{margin:0;padding:0;border:1px solid #e2e2e2;border-left-width:0}.laydate-time-list li:first-child ol{border-left-width:1px}.laydate-time-list>li:hover{background:0 0}.layui-laydate-content .laydate-day-next,.layui-laydate-content .laydate-day-prev{color:#d2d2d2}.laydate-selected.laydate-day-next,.laydate-selected.laydate-day-prev{background-color:#f8f8f8!important}.layui-laydate-footer{border-top:1px solid #e2e2e2}.layui-laydate-hint{color:#ff5722}.laydate-day-mark::after{background-color:#5fb878}.layui-laydate-content td.layui-this .laydate-day-mark::after{display:none}.layui-laydate-footer span[lay-type=date]{color:#5fb878}.layui-laydate .layui-this{background-color:#009688!important;color:#fff!important}.layui-laydate .laydate-disabled,.layui-laydate .laydate-disabled:hover{background:0 0!important;color:#d2d2d2!important;cursor:not-allowed!important;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.laydate-theme-molv{border:none}.laydate-theme-molv.layui-laydate-range{width:548px}.laydate-theme-molv .layui-laydate-main{width:274px}.laydate-theme-molv .layui-laydate-header{border:none;background-color:#009688}.laydate-theme-molv .layui-laydate-header i,.laydate-theme-molv .layui-laydate-header span{color:#f6f6f6}.laydate-theme-molv .layui-laydate-header i:hover,.laydate-theme-molv .layui-laydate-header span:hover{color:#fff}.laydate-theme-molv .layui-laydate-content{border:1px solid #e2e2e2;border-top:none;border-bottom:none}.laydate-theme-molv .laydate-main-list-1 .layui-laydate-content{border-left:none}.laydate-theme-molv .layui-laydate-footer{border:1px solid #e2e2e2}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li,.laydate-theme-grid .layui-laydate-content td,.laydate-theme-grid .layui-laydate-content thead{border:1px solid #e2e2e2}.laydate-theme-grid .laydate-selected,.laydate-theme-grid .laydate-selected:hover{background-color:#f2f2f2!important;color:#009688!important}.laydate-theme-grid .laydate-selected.laydate-day-next,.laydate-theme-grid .laydate-selected.laydate-day-prev{color:#d2d2d2!important}.laydate-theme-grid .laydate-month-list,.laydate-theme-grid .laydate-year-list{margin:1px 0 0 1px}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li{margin:0 -1px -1px 0}.laydate-theme-grid .laydate-year-list>li{height:43px;line-height:43px}.laydate-theme-grid .laydate-month-list>li{height:71px;line-height:71px} diff --git a/module/static/layui/css/modules/layer/default/icon-ext.png b/module/static/layui/css/modules/layer/default/icon-ext.png new file mode 100644 index 0000000..bbbb669 Binary files /dev/null and b/module/static/layui/css/modules/layer/default/icon-ext.png differ diff --git a/module/static/layui/css/modules/layer/default/icon.png b/module/static/layui/css/modules/layer/default/icon.png new file mode 100644 index 0000000..3e17da8 Binary files /dev/null and b/module/static/layui/css/modules/layer/default/icon.png differ diff --git a/module/static/layui/css/modules/layer/default/layer.css b/module/static/layui/css/modules/layer/default/layer.css new file mode 100644 index 0000000..164b207 --- /dev/null +++ b/module/static/layui/css/modules/layer/default/layer.css @@ -0,0 +1 @@ +html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch}.layui-layer{top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #b2b2b2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) #eee center center no-repeat}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:50px;line-height:50px;border-bottom:1px solid #f0f0f0;font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:17px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2e2d3c;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2d93ca}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:5px 5px 0;padding:0 15px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#1e9fff;background-color:#1e9fff;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:300px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8d8d8d;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #d3d4d3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476a7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #e9e7e7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#e9e7e7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#c9c5c5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92b8b1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:260px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:300px;padding:0 20px;text-align:center;cursor:default;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:51px;border-left:1px solid #eee;border-right:1px solid #eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{background:0 0;box-shadow:none}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgnext,.layui-layer-imgprev{position:fixed;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:30px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:30px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:fixed;left:0;right:0;bottom:0;width:100%;height:40px;line-height:40px;background-color:#000\9;filter:Alpha(opacity=60);background-color:rgba(2,0,0,.35);color:#fff;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} diff --git a/module/static/layui/css/modules/layer/default/loading-0.gif b/module/static/layui/css/modules/layer/default/loading-0.gif new file mode 100644 index 0000000..6f3c953 Binary files /dev/null and b/module/static/layui/css/modules/layer/default/loading-0.gif differ diff --git a/module/static/layui/css/modules/layer/default/loading-1.gif b/module/static/layui/css/modules/layer/default/loading-1.gif new file mode 100644 index 0000000..db3a483 Binary files /dev/null and b/module/static/layui/css/modules/layer/default/loading-1.gif differ diff --git a/module/static/layui/css/modules/layer/default/loading-2.gif b/module/static/layui/css/modules/layer/default/loading-2.gif new file mode 100644 index 0000000..5bb90fd Binary files /dev/null and b/module/static/layui/css/modules/layer/default/loading-2.gif differ diff --git a/module/static/layui/font/iconfont.eot b/module/static/layui/font/iconfont.eot new file mode 100644 index 0000000..622d7ec Binary files /dev/null and b/module/static/layui/font/iconfont.eot differ diff --git a/module/static/layui/font/iconfont.svg b/module/static/layui/font/iconfont.svg new file mode 100644 index 0000000..c757472 --- /dev/null +++ b/module/static/layui/font/iconfont.svg @@ -0,0 +1,554 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module/static/layui/font/iconfont.ttf b/module/static/layui/font/iconfont.ttf new file mode 100644 index 0000000..06e30f9 Binary files /dev/null and b/module/static/layui/font/iconfont.ttf differ diff --git a/module/static/layui/font/iconfont.woff b/module/static/layui/font/iconfont.woff new file mode 100644 index 0000000..66a1783 Binary files /dev/null and b/module/static/layui/font/iconfont.woff differ diff --git a/module/static/layui/font/iconfont.woff2 b/module/static/layui/font/iconfont.woff2 new file mode 100644 index 0000000..47e9980 Binary files /dev/null and b/module/static/layui/font/iconfont.woff2 differ diff --git a/module/static/layui/layui.js b/module/static/layui/layui.js new file mode 100644 index 0000000..d640d93 --- /dev/null +++ b/module/static/layui/layui.js @@ -0,0 +1 @@ +/** 2.7.6 | MIT Licensed */;!function(d){"use strict";var t,h=d.document,m={modules:{},status:{},timeout:10,event:{}},r=function(){this.v="2.7.6"},e=d.LAYUI_GLOBAL||{},v=(t=h.currentScript?h.currentScript.src:function(){for(var t,e=h.scripts,o=e.length-1,r=o;01e3*m.timeout/4?g(u+" is not a valid module","error"):void(m.status[u]?c():setTimeout(r,4))}())}function c(){e.push(layui[u]),11e3*m.timeout/4?g(u+" is not a valid module","error"):void("string"==typeof m.modules[u]&&m.status[u]?c():setTimeout(f,4))}():((p=h.createElement("script"))["async"]=!0,p.charset="utf-8",p.src=y+((i=!0===m.version?m.v||(new Date).getTime():m.version||"")?"?v="+i:""),a.appendChild(p),!p.attachEvent||p.attachEvent.toString&&p.attachEvent.toString().indexOf("[native code")<0||b?p.addEventListener("load",function(t){s(t,y)},!1):p.attachEvent("onreadystatechange",function(t){s(t,y)}),m.modules[u]=y),n},r.prototype.disuse=function(t){var o=this;return t=o.isArray(t)?t:[t],o.each(t,function(t,e){m.status[e],delete o[e],delete N[e],delete o.modules[e],delete m.status[e],delete m.modules[e]}),o},r.prototype.getStyle=function(t,e){t=t.currentStyle||d.getComputedStyle(t,null);return t[t.getPropertyValue?"getPropertyValue":"getAttribute"](e)},r.prototype.link=function(o,r,t){var n=this,e=h.getElementsByTagName("head")[0],i=h.createElement("link"),t=((t="string"==typeof r?r:t)||o).replace(/\.|\//g,""),a=i.id="layuicss-"+t,u="creating",l=0;return i.rel="stylesheet",i.href=o+(m.debug?"?v="+(new Date).getTime():""),i.media="all",h.getElementById(a)||e.appendChild(i),"function"!=typeof r||function s(t){var e=h.getElementById(a);return++l>1e3*m.timeout/100?g(o+" timeout"):void(1989===parseInt(n.getStyle(e,"width"))?(t===u&&e.removeAttribute("lay-status"),e.getAttribute("lay-status")===u?setTimeout(s,100):r()):(e.setAttribute("lay-status",u),setTimeout(function(){s(u)},100)))}(),n},r.prototype.addcss=function(t,e,o){return layui.link(m.dir+"css/"+t,e,o)},m.callback={},r.prototype.factory=function(t){if(layui[t])return"function"==typeof m.callback[t]?m.callback[t]:null},r.prototype.img=function(t,e,o){var r=new Image;if(r.src=t,r.complete)return e(r);r.onload=function(){r.onload=null,"function"==typeof e&&e(r)},r.onerror=function(t){r.onerror=null,"function"==typeof o&&o(t)}},r.prototype.config=function(t){for(var e in t=t||{})m[e]=t[e];return this},r.prototype.modules=function(){var t,e={};for(t in N)e[t]=N[t];return e}(),r.prototype.extend=function(t){for(var e in t=t||{})this[e]||this.modules[e]?g(e+" Module already exists","error"):this.modules[e]=t[e];return this},r.prototype.router=r.prototype.hash=function(t){var o={path:[],search:{},hash:((t=t||location.hash).match(/[^#](#.*$)/)||[])[1]||""};return/^#\//.test(t)&&(t=t.replace(/^#\//,""),o.href="/"+t,t=t.replace(/([^#])(#.*$)/,"$1").split("/")||[],this.each(t,function(t,e){/^\w+=/.test(e)?(e=e.split("="),o.search[e[0]]=e[1]):o.path.push(e)})),o},r.prototype.url=function(t){var n,e,o=this;return{pathname:(t?((t.match(/\.[^.]+?\/.+/)||[])[0]||"").replace(/^[^\/]+/,"").replace(/\?.+/,""):location.pathname).replace(/^\//,"").split("/"),search:(n={},e=(t?((t.match(/\?.+/)||[])[0]||"").replace(/\#.+/,""):location.search).replace(/^\?+/,"").split("&"),o.each(e,function(t,e){var o=e.indexOf("="),r=o<0?e.substr(0,e.length):0!==o&&e.substr(0,o);r&&(n[r]=0(l.innerHeight||f.documentElement.clientHeight)},h.position=function(t,e,n){var o,i,r,c,u,a,s;e&&(n=n||{},t!==f&&t!==h("body")[0]||(n.clickType="right"),u="right"===n.clickType?{left:(u=n.e||l.event||{}).clientX,top:u.clientY,right:u.clientX,bottom:u.clientY}:t.getBoundingClientRect(),a=e.offsetWidth,s=e.offsetHeight,o=function(t){return f.body[t=t?"scrollLeft":"scrollTop"]|f.documentElement[t]},r=u.left,c=u.bottom,"center"===n.align?r-=(a-t.offsetWidth)/2:"right"===n.align&&(r=r-a+t.offsetWidth),(r=r+a+5>(i=function(t){return f.documentElement[t?"clientWidth":"clientHeight"]})("width")?i("width")-a-5:r)<5&&(r=5),c+s+5>i()&&(u.top>s+5?c=u.top-s-10:"right"===n.clickType?(c=i()-s-10)<0&&(c=0):c=5),(a=n.position)&&(e.style.position=a),e.style.left=r+("fixed"===a?0:o(1))+"px",e.style.top=c+("fixed"===a?0:o())+"px",h.hasScrollbar()||(s=e.getBoundingClientRect(),!n.SYSTEM_RELOAD&&s.bottom+5>i()&&(n.SYSTEM_RELOAD=!0,setTimeout(function(){h.position(t,e,n)},50))))},h.options=function(t,e){t=h(t),e=e||"lay-options";try{return new Function("return "+(t.attr(e)||"{}"))()}catch(n){return hint.error("parseerror\uff1a"+n,"error"),{}}},h.isTopElem=function(n){var t=[f,h("body")[0]],o=!1;return h.each(t,function(t,e){if(e===n)return o=!0}),o},i.addStr=function(n,t){return n=n.replace(/\s+/," "),t=t.replace(/\s+/," ").split(" "),h.each(t,function(t,e){new RegExp("\\b"+e+"\\b").test(n)||(n=n+" "+e)}),n.replace(/^\s|\s$/,"")},i.removeStr=function(n,t){return n=n.replace(/\s+/," "),t=t.replace(/\s+/," ").split(" "),h.each(t,function(t,e){e=new RegExp("\\b"+e+"\\b");e.test(n)&&(n=n.replace(e,""))}),n.replace(/\s+/," ").replace(/^\s|\s$/,"")},i.prototype.find=function(o){var i=this,r=0,c=[],u="object"==typeof o;return this.each(function(t,e){for(var n=u?e.contains(o):e.querySelectorAll(o||null);r]|&(?=#[a-zA-Z0-9]+)/g.test(e+="")?e.replace(/&(?!#?[a-zA-Z0-9]+;)/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,"""):e},error:function(e,r){var n="Laytpl Error: ";return"object"==typeof console&&console.error(n+e+"\n"+(r||"")),n+e}},l=a.exp,r=function(e){this.tpl=e},n=(r.pt=r.prototype,window.errors=0,r.pt.parse=function(e,r){var n=e,c=l("^"+p.open+"#",""),t=l(p.close+"$","");e='"use strict";var view = "'+(e=e.replace(/\s+|\r|\t|\n/g," ").replace(l(p.open+"#"),p.open+"# ").replace(l(p.close+"}"),"} "+p.close).replace(/\\/g,"\\\\").replace(l(p.open+"!(.+?)!"+p.close),function(e){return e=e.replace(l("^"+p.open+"!"),"").replace(l("!"+p.close),"").replace(l(p.open+"|"+p.close),function(e){return e.replace(/(.)/g,"\\$1")})}).replace(/(?="|')/g,"\\").replace(a.query(),function(e){return'";'+(e=e.replace(c,"").replace(t,"")).replace(/\\(.)/g,"$1")+';view+="'}).replace(a.query(1),function(e){var r='"+laytpl.escape(';return e.replace(/\s/g,"")===p.open+p.close?"":(e=e.replace(l(p.open+"|"+p.close),""),/^=/.test(e)?e=e.replace(/^=/,""):/^-/.test(e)&&(e=e.replace(/^-/,""),r='"+('),r+e.replace(/\\(.)/g,"$1")+')+"')}))+'";return view;';try{return this.cache=e=new Function("d, laytpl",e),e(r,a)}catch(o){return delete this.cache,a.error(o,n)}},r.pt.render=function(e,r){var n=this;return e?(e=n.cache?n.cache(e,a):n.parse(n.tpl,e),r?void r(e):e):a.error("no data")},function(e){return"string"!=typeof e?a.error("Template not found"):new r(e)});n.config=function(e){for(var r in e=e||{})p[r]=e[r]},n.v="1.2.0",e("laytpl",n)});layui.define(function(e){"use strict";var n=document,u="getElementById",c="getElementsByTagName",a="layui-disabled",t=function(e){var a=this;a.config=e||{},a.config.index=++o.index,a.render(!0)},o=(t.prototype.type=function(){var e=this.config;if("object"==typeof e.elem)return e.elem.length===undefined?2:3},t.prototype.view=function(){var t,i,r=this.config,n=r.groups="groups"in r?Number(r.groups)||0:5,u=(r.layout="object"==typeof r.layout?r.layout:["prev","page","next"],r.count=Number(r.count)||0,r.curr=Number(r.curr)||1,r.limits="object"==typeof r.limits?r.limits:[10,20,30,40,50],r.limit=Number(r.limit)||10,r.pages=Math.ceil(r.count/r.limit)||1,r.curr>r.pages?r.curr=r.pages:r.curr<1&&(r.curr=1),n<0?n=1:n>r.pages&&(n=r.pages),r.prev="prev"in r?r.prev:"上一页",r.next="next"in r?r.next:"下一页",r.pages>n?Math.ceil((r.curr+(1'+r.prev+"":"",page:function(){var e=[];if(r.count<1)return"";1'+(r.first||1)+"");var a=Math.floor((n-1)/2),t=1r.pages?r.pages:a:n;for(i-t…');t<=i;t++)t===r.curr?e.push('"+t+""):e.push(''+t+"");return r.pages>n&&r.pages>i&&!1!==r.last&&(i+1…'),0!==n&&e.push(''+(r.last||r.pages)+"")),e.join("")}(),next:r.next?''+r.next+"":"",count:'\u5171 '+r.count+" \u6761",limit:(t=['"),refresh:['','',""].join(""),skip:['到第','','页',""].join("")};return['
',(i=[],layui.each(r.layout,function(e,a){s[a]&&i.push(s[a])}),i.join("")),"
"].join("")},t.prototype.jump=function(e,a){if(e){var t=this,i=t.config,r=e.children,n=e[c]("button")[0],u=e[c]("input")[0],e=e[c]("select")[0],s=function(){var e=Number(u.value.replace(/\s|\D/g,""));e&&(i.curr=e,t.render())};if(a)return s();for(var l=0,p=r.length;li.pages||(i.curr=e,t.render())});e&&o.on(e,"change",function(){var e=this.value;i.curr*e>i.count&&(i.curr=Math.ceil(i.count/e)),i.limit=e,t.render()}),n&&o.on(n,"click",function(){s()})}},t.prototype.skip=function(t){var i,e;t&&(i=this,(e=t[c]("input")[0])&&o.on(e,"keyup",function(e){var a=this.value,e=e.keyCode;/^(37|38|39|40)$/.test(e)||(/\D/.test(a)&&(this.value=a.replace(/\D/,"")),13===e&&i.jump(t,!0))}))},t.prototype.render=function(e){var a=this,t=a.config,i=a.type(),r=a.view(),i=(2===i?t.elem&&(t.elem.innerHTML=r):3===i?t.elem.html(r):n[u](t.elem)&&(n[u](t.elem).innerHTML=r),t.jump&&t.jump(t,e),n[u]("layui-laypage-"+t.index));a.jump(i),t.hash&&!e&&(location.hash="!"+t.hash+"="+t.curr),a.skip(i)},{render:function(e){return new t(e).index},index:layui.laypage?layui.laypage.index+1e4:0,on:function(a,e,t){return a.attachEvent?a.attachEvent("on"+e,function(e){e.target=e.srcElement,t.call(a,e)}):a.addEventListener(e,t,!1),this}});e("laypage",o)});!function(i,r){"use strict";var n=i.layui&&layui.define,l={getPath:i.lay&&lay.getPath?lay.getPath:"",link:function(e,t,a){u.path&&i.lay&&lay.layui&&lay.layui.link(u.path+e,t,a)}},e=i.LAYUI_GLOBAL||{},u={v:"5.3.1",config:{weekStart:0},index:i.laydate&&i.laydate.v?1e5:0,path:e.laydate_dir||l.getPath,set:function(e){var t=this;return t.config=lay.extend({},t.config,e),t},ready:function(e){var t="laydate",a=(n?"modules/laydate/":"theme/")+"default/laydate.css?v="+u.v;return n?layui.addcss(a,e,t):l.link(a,e,t),this}},s=function(){var t=this,e=t.config.id;return{hint:function(e){t.hint.call(t,e)},config:(s.that[e]=t).config}},a="laydate",w="layui-this",x="laydate-disabled",h=[100,2e5],p="layui-laydate-static",M="layui-laydate-list",o="layui-laydate-hint",E=".laydate-btns-confirm",C="laydate-time-text",k="laydate-btns-time",f="layui-laydate-preview",g=function(e){var t=this,a=(t.index=++u.index,t.config=lay.extend({},t.config,u.config,e),lay(e.elem||t.config.elem));if(1\u8bf7\u91cd\u65b0\u9009\u62e9",invalidDate:"\u4e0d\u5728\u6709\u6548\u65e5\u671f\u6216\u65f6\u95f4\u8303\u56f4\u5185",formatError:["\u65e5\u671f\u683c\u5f0f\u4e0d\u5408\u6cd5
\u5fc5\u987b\u9075\u5faa\u4e0b\u8ff0\u683c\u5f0f\uff1a
","
\u5df2\u4e3a\u4f60\u91cd\u7f6e"],preview:"\u5f53\u524d\u9009\u4e2d\u7684\u7ed3\u679c"},en:{weeks:["Su","Mo","Tu","We","Th","Fr","Sa"],time:["Hours","Minutes","Seconds"],timeTips:"Select Time",startTime:"Start Time",endTime:"End Time",dateTips:"Select Date",month:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],tools:{confirm:"Confirm",clear:"Clear",now:"Now"},timeout:"End time cannot be less than start Time
Please re-select",invalidDate:"Invalid date",formatError:["The date format error
Must be followed\uff1a
","
It has been reset"],preview:"The selected result"}};return e[this.config.lang]||e.cn},g.prototype.init=function(){var r=this,o=r.config,e="static"===o.position,t={year:"yyyy",month:"yyyy-MM",date:"yyyy-MM-dd",time:"HH:mm:ss",datetime:"yyyy-MM-dd HH:mm:ss"};o.elem=lay(o.elem),o.eventElem=lay(o.eventElem),o.elem[0]&&(r.rangeStr=o.range?"string"==typeof o.range?o.range:"-":"","array"===layui.type(o.range)&&(r.rangeElem=[lay(o.range[0]),lay(o.range[1])]),t[o.type]||(i.console&&console.error&&console.error("laydate type error:'"+o.type+"' is not supported"),o.type="date"),o.format===t.date&&(o.format=t[o.type]||t.date),r.format=s.formatArr(o.format),o.weekStart&&!/^[0-6]$/.test(o.weekStart)&&(t=r.lang(),o.weekStart=t.weeks.indexOf(o.weekStart),-1===o.weekStart&&(o.weekStart=0)),r.EXP_IF="",r.EXP_SPLIT="",lay.each(r.format,function(e,t){e=new RegExp(y).test(t)?"\\d{"+(new RegExp(y).test(r.format[0===e?e+1:e-1]||"")?/^yyyy|y$/.test(t)?4:t.length:/^yyyy$/.test(t)?"1,4":/^y$/.test(t)?"1,308":"1,2")+"}":"\\"+t;r.EXP_IF=r.EXP_IF+e,r.EXP_SPLIT=r.EXP_SPLIT+"("+e+")"}),r.EXP_IF_ONE=new RegExp("^"+r.EXP_IF+"$"),r.EXP_IF=new RegExp("^"+(o.range?r.EXP_IF+"\\s\\"+r.rangeStr+"\\s"+r.EXP_IF:r.EXP_IF)+"$"),r.EXP_SPLIT=new RegExp("^"+r.EXP_SPLIT+"$",""),r.isInput(o.elem[0])||"focus"===o.trigger&&(o.trigger="click"),o.elem.attr("lay-key")||(o.elem.attr("lay-key",r.index),o.eventElem.attr("lay-key",r.index)),o.mark=lay.extend({},o.calendar&&"cn"===o.lang?{"0-1-1":"\u5143\u65e6","0-2-14":"\u60c5\u4eba","0-3-8":"\u5987\u5973","0-3-12":"\u690d\u6811","0-4-1":"\u611a\u4eba","0-5-1":"\u52b3\u52a8","0-5-4":"\u9752\u5e74","0-6-1":"\u513f\u7ae5","0-9-10":"\u6559\u5e08","0-10-1":"\u56fd\u5e86","0-12-25":"\u5723\u8bde"}:{},o.mark),lay.each(["min","max"],function(e,t){var a,n,i=[],l=[];l="number"==typeof o[t]?(n=o[t],a=new Date,a=r.newDate({year:a.getFullYear(),month:a.getMonth(),date:a.getDate(),hours:"23",minutes:"59",seconds:"59"}).getTime(),i=[(n=new Date(n?n<864e5?a+864e5*n:n:a)).getFullYear(),n.getMonth()+1,n.getDate()],[n.getHours(),n.getMinutes(),n.getSeconds()]):(i=(o[t].match(/\d+-\d+-\d+/)||[""])[0].split("-"),(o[t].match(/\d+:\d+:\d+/)||[""])[0].split(":")),o[t]={year:0|i[0]||(new Date).getFullYear(),month:i[1]?(0|i[1])-1:(new Date).getMonth(),date:0|i[2]||(new Date).getDate(),hours:0|l[0],minutes:0|l[1],seconds:0|l[2]}}),r.elemID="layui-laydate"+o.elem.attr("lay-key"),(o.show||e)&&r.render(),e||r.events(),o.value&&o.isInitValue&&("date"===layui.type(o.value)?r.setValue(r.parse(0,r.systemDate(o.value))):r.setValue(o.value)))},g.prototype.render=function(){var n,e,t=this,o=t.config,s=t.lang(),i="static"===o.position,a=t.elem=lay.elem("div",{id:t.elemID,"class":["layui-laydate",o.range?" layui-laydate-range":"",i?" "+p:"",o.theme&&"default"!==o.theme&&!/^#/.test(o.theme)?" laydate-theme-"+o.theme:""].join("")}),y=t.elemMain=[],d=t.elemHeader=[],m=t.elemCont=[],c=t.table=[],l=t.footer=lay.elem("div",{"class":"layui-laydate-footer"});o.zIndex&&(a.style.zIndex=o.zIndex),lay.each(new Array(2),function(e){if(!o.range&&0'+s.timeTips+""),!o.range&&"datetime"===o.type||e.push(''),lay.each(o.btns,function(e,t){var a=s.tools[t]||"btn";o.range&&"now"===t||(i&&"clear"===t&&(a="cn"===o.lang?"\u91cd\u7f6e":"Reset"),n.push(''+a+""))}),e.push('"),e.join(""))),lay.each(y,function(e,t){a.appendChild(t)}),o.showBottom&&a.appendChild(l),/^#/.test(o.theme)&&(e=lay.elem("style"),l=["#{{id}} .layui-laydate-header{background-color:{{theme}};}","#{{id}} .layui-this{background-color:{{theme}} !important;}"].join("").replace(/{{id}}/g,t.elemID).replace(/{{theme}}/g,o.theme),"styleSheet"in e?(e.setAttribute("type","text/css"),e.styleSheet.cssText=l):e.innerHTML=l,lay(a).addClass("laydate-theme-molv"),a.appendChild(e)),t.remove(g.thisElemDate),u.thisId=o.id,i?o.elem.append(a):(r.body.appendChild(a),t.position()),t.checkDate().calendar(null,0,"init"),t.changeEvent(),g.thisElemDate=t.elemID,"function"==typeof o.ready&&o.ready(lay.extend({},o.dateTime,{month:o.dateTime.month+1})),t.preview()},g.prototype.remove=function(e){var t=this,a=t.config,n=lay("#"+(e||t.elemID));return n[0]&&(n.hasClass(p)||t.checkDate(function(){n.remove(),delete u.thisId,"function"==typeof a.close&&a.close(t)})),t},g.prototype.position=function(){var e=this.config;return lay.position(this.bindElem||e.elem[0],this.elem,{position:e.position}),this},g.prototype.hint=function(e){var t=this,a=(t.config,lay.elem("div",{"class":o}));t.elem&&(a.innerHTML=e||"",lay(t.elem).find("."+o).remove(),t.elem.appendChild(a),clearTimeout(t.hinTimer),t.hinTimer=setTimeout(function(){lay(t.elem).find("."+o).remove()},3e3))},g.prototype.getAsYM=function(e,t,a){return a?t--:t++,t<0&&(t=11,e--),11h[1]&&(e.year=h[1],o=!0),11t&&(e.date=t,o=!0)},r=function(n,i,l){var r=["startTime","endTime"];i=(i.match(s.EXP_SPLIT)||[]).slice(1),l=l||0,y.range&&(s[r[l]]=s[r[l]]||{}),lay.each(s.format,function(e,t){var a=parseFloat(i[e]);i[e].lengths.getDateTime(y.max)?n=y.dateTime=lay.extend({},y.max):s.getDateTime(n)s.getDateTime(y.max))&&(s.endDate=lay.extend({},y.max)),s.startTime={hours:y.dateTime.hours,minutes:y.dateTime.minutes,seconds:y.dateTime.seconds},s.endTime={hours:s.endDate.hours,minutes:s.endDate.minutes,seconds:s.endDate.seconds}),e&&e(),s},g.prototype.mark=function(e,a){var n,t=this.config;return lay.each(t.mark,function(e,t){e=e.split("-");e[0]!=a[0]&&0!=e[0]||e[1]!=a[1]&&0!=e[1]||e[2]!=a[2]||(n=t||a[2])}),n&&e.html(''+n+""),this},g.prototype.holidays=function(n,i){var e=this.config,l=["","work"];return"array"!==layui.type(e.holidays)||lay.each(e.holidays,function(a,e){lay.each(e,function(e,t){t===n.attr("lay-ymd")&&n.html('"+i[2]+"")})}),this},g.prototype.limit=function(e,t,a,i){var l=this,n=l.config,r={},a=(i?0:41)r.max,e&&e[t?"addClass":"removeClass"](x),t},g.prototype.thisDateTime=function(e){var t=this.config;return e?this.endDate:t.dateTime},g.prototype.calendar=function(e,t,a){var i,l,r,o=this,n=o.config,t=t?1:0,s=e||o.thisDateTime(t),y=new Date,d=o.lang(),m="date"!==n.type&&"datetime"!==n.type,c=lay(o.table[t]).find("td"),t=lay(o.elemHeader[t][2]).find("span");return s.yearh[1]&&(s.year=h[1],o.hint(d.invalidDate)),o.firstDate||(o.firstDate=lay.extend({},s)),y.setFullYear(s.year,s.month,1),i=(y.getDay()+(7-n.weekStart))%7,l=u.getEndDate(s.month||12,s.year),r=u.getEndDate(s.month+1,s.year),lay.each(c,function(e,t){var a=[s.year,s.month],n=0;(t=lay(t)).removeAttr("class"),e"+d.time[t]+"

    "];lay.each(new Array(e),function(e){n.push(""+lay.digit(e,2)+"")}),a.innerHTML=n.join("")+"
",m.appendChild(a)}),l()),p&&h.removeChild(p),h.appendChild(m),"year"===t||"month"===t?(lay(o.elemMain[n]).addClass("laydate-ym-show"),lay(m).find("li").on("click",function(){var e=0|lay(this).attr("lay-ym");lay(this).hasClass(x)||(0===n?(y[t]=e,o.limit(lay(o.footer).find(E),null,0)):o.endDate[t]=e,"year"===s.type||"month"===s.type?(lay(m).find("."+w).removeClass(w),lay(this).addClass(w),"month"===s.type&&"year"===t&&(o.listYM[n][0]=e,a&&((n?o.endDate:y).year=e),o.list("month",n))):(o.checkDate("limit").calendar(null,n),o.closeList()),o.setBtnStatus(),s.range||("month"===s.type&&"month"===t||"year"===s.type&&"year"===t)&&o.setValue(o.parse()).remove().done(),o.done(null,"change"),lay(o.footer).find("."+k).removeClass(x))})):(e=lay.elem("span",{"class":C}),r=function(){lay(m).find("ol").each(function(e){var a=this,t=lay(a).find("li");a.scrollTop=30*(o[D][T[e]]-2),a.scrollTop<=0&&t.each(function(e,t){if(!lay(this).hasClass(x))return a.scrollTop=30*(e-2),!0})})},u=lay(c[2]).find("."+C),r(),e.innerHTML=s.range?[d.startTime,d.endTime][n]:d.timeTips,lay(o.elemMain[n]).addClass("laydate-time-show"),u[0]&&u.remove(),c[2].appendChild(e),lay(m).find("ol").each(function(t){var a=this;lay(a).find("li").on("click",function(){var e=0|this.innerHTML;lay(this).hasClass(x)||(s.range?o[D][T[t]]=e:y[T[t]]=e,lay(a).find("."+w).removeClass(w),lay(this).addClass(w),l(),r(),!o.endDate&&"time"!==s.type||o.done(null,"change"),o.setBtnStatus())})})),o},g.prototype.listYM=[],g.prototype.closeList=function(){var a=this;a.config;lay.each(a.elemCont,function(e,t){lay(this).find("."+M).remove(),lay(a.elemMain[e]).removeClass("laydate-ym-show laydate-time-show")}),lay(a.elem).find("."+C).remove()},g.prototype.setBtnStatus=function(e,t,a){var n=this,i=n.config,l=n.lang(),r=lay(n.footer).find(E);i.range&&"time"!==i.type&&(t=t||i.dateTime,a=a||n.endDate,i=n.newDate(t).getTime()>n.newDate(a).getTime(),n.limit(null,t)||n.limit(null,a)?r.addClass(x):r[i?"addClass":"removeClass"](x),e&&i&&n.hint("string"==typeof e?l.timeout.replace(/\u65e5\u671f/g,e):l.timeout))},g.prototype.parse=function(e,t){var a=this,n=a.config,t=t||("end"==e?lay.extend({},a.endDate,a.endTime):n.range?lay.extend({},n.dateTime,a.startTime):n.dateTime),t=u.parse(t,a.format,1);return n.range&&e===undefined?t+" "+a.rangeStr+" "+a.parse("end"):t},g.prototype.newDate=function(e){return e=e||{},new Date(e.year||1,e.month||0,e.date||1,e.hours||0,e.minutes||0,e.seconds||0)},g.prototype.getDateTime=function(e){return this.newDate(e).getTime()},g.prototype.setValue=function(e){var t=this,a=t.config,n=t.bindElem||a.elem[0];return"static"===a.position||(e=e||"",t.isInput(n)?lay(n).val(e):(a=t.rangeElem)?("array"!==layui.type(e)&&(e=e.split(" "+t.rangeStr+" ")),a[0].val(e[0]||""),a[1].val(e[1]||"")):(0===lay(n).find("*").length&&lay(n).html(e),lay(n).attr("lay-date",e))),t},g.prototype.preview=function(){var e,t=this,a=t.config;a.isPreview&&(e=lay(t.elem).find("."+f),a=!a.range||t.endDate?t.parse():"",e.html(a).css({color:"#5FB878"}),setTimeout(function(){e.css({color:"#666"})},300))},g.prototype.done=function(e,t){var a=this,n=a.config,i=lay.extend({},lay.extend(n.dateTime,a.startTime)),l=lay.extend({},lay.extend(a.endDate,a.endTime));return lay.each([i,l],function(e,t){"month"in t&&lay.extend(t,{month:t.month+1})}),a.preview(),e=e||[a.parse(),i,l],"function"==typeof n[t||"done"]&&n[t||"done"].apply(n,e),a},g.prototype.choose=function(e,a){var n=this,i=n.config,l=n.thisDateTime(a),t=(lay(n.elem).find("td"),{year:0|(t=e.attr("lay-ymd").split("-"))[0],month:(0|t[1])-1,date:0|t[2]});e.hasClass(x)||(lay.extend(l,t),i.range?(lay.each(["startTime","endTime"],function(e,t){n[t]=n[t]||{hours:e?23:0,minutes:e?59:0,seconds:e?59:0},a===e&&(n.getDateTime(lay.extend({},l,n[t]))n.getDateTime(i.max)&&(n[t]={hours:i.max.hours,minutes:i.max.minutes,seconds:i.max.seconds},lay.extend(l,n[t])))}),n.calendar(null,a).done(null,"change")):"static"===i.position?n.calendar().done().done(null,"change"):"date"===i.type?n.setValue(n.parse()).remove().done():"datetime"===i.type&&n.calendar().done(null,"change"))},g.prototype.tool=function(e,t){var a=this,n=a.config,i=a.lang(),l=n.dateTime,r="static"===n.position,o={datetime:function(){lay(e).hasClass(x)||(a.list("time",0),n.range&&a.list("time",1),lay(e).attr("lay-type","date").html(a.lang().dateTips))},date:function(){a.closeList(),lay(e).attr("lay-type","datetime").html(a.lang().timeTips)},clear:function(){r&&(lay.extend(l,a.firstDate),a.calendar()),n.range&&(delete n.dateTime,delete a.endDate,delete a.startTime,delete a.endTime),a.setValue("").remove(),a.done(["",{},{}])},now:function(){var e=new Date;lay.extend(l,a.systemDate(),{hours:e.getHours(),minutes:e.getMinutes(),seconds:e.getSeconds()}),a.setValue(a.parse()).remove(),r&&a.calendar(),a.done()},confirm:function(){if(n.range){if(lay(e).hasClass(x))return a.hint("time"===n.type?i.timeout.replace(/\u65e5\u671f/g,"\u65f6\u95f4"):i.timeout)}else if(lay(e).hasClass(x))return a.hint(i.invalidDate);a.setValue(a.parse()).remove(),a.done()}};o[t]&&o[t]()},g.prototype.change=function(n){var i=this,l=i.config,r=i.thisDateTime(n),o=l.range&&("year"===l.type||"month"===l.type),s=i.elemCont[n||0],y=i.listYM[n],e=function(e){var t=lay(s).find(".laydate-year-list")[0],a=lay(s).find(".laydate-month-list")[0];return t&&(y[0]=e?y[0]-15:y[0]+15,i.list("year",n)),a&&(e?y[0]--:y[0]++,i.list("month",n)),(t||a)&&(lay.extend(r,{year:y[0]}),o&&(r.year=y[0]),l.range||i.done(null,"change"),l.range||i.limit(lay(i.footer).find(E),{year:y[0]})),i.setBtnStatus(),t||a};return{prevYear:function(){e("sub")||(r.year--,i.checkDate("limit").calendar(null,n),i.done(null,"change"))},prevMonth:function(){var e=i.getAsYM(r.year,r.month,"sub");lay.extend(r,{year:e[0],month:e[1]}),i.checkDate("limit").calendar(null,n),i.done(null,"change")},nextMonth:function(){var e=i.getAsYM(r.year,r.month);lay.extend(r,{year:e[0],month:e[1]}),i.checkDate("limit").calendar(null,n),i.done(null,"change")},nextYear:function(){e()||(r.year++,i.checkDate("limit").calendar(null,n),i.done(null,"change"))}}},g.prototype.changeEvent=function(){var i=this;i.config;lay(i.elem).on("click",function(e){lay.stope(e)}).on("mousedown",function(e){lay.stope(e)}),lay.each(i.elemHeader,function(n,e){lay(e[0]).on("click",function(e){i.change(n).prevYear()}),lay(e[1]).on("click",function(e){i.change(n).prevMonth()}),lay(e[2]).find("span").on("click",function(e){var t=lay(this),a=t.attr("lay-ym"),t=t.attr("lay-type");a&&(a=a.split("-"),i.listYM[n]=[0|a[0],0|a[1]],i.list(t,n),lay(i.footer).find("."+k).addClass(x))}),lay(e[3]).on("click",function(e){i.change(n).nextMonth()}),lay(e[4]).on("click",function(e){i.change(n).nextYear()})}),lay.each(i.table,function(e,t){lay(t).find("td").on("click",function(){i.choose(lay(this),e)})}),lay(i.footer).find("span").on("click",function(){var e=lay(this).attr("lay-type");i.tool(this,e)})},g.prototype.isInput=function(e){return/input|textarea/.test(e.tagName.toLocaleLowerCase())||/INPUT|TEXTAREA/.test(e.tagName)},g.prototype.events=function(){var a=this,n=a.config,e=function(e,t){e.on(n.trigger,function(){u.thisId!==n.id&&(t&&(a.bindElem=this),a.render())})};n.elem[0]&&!n.elem[0].eventHandler&&(e(n.elem,"bind"),e(n.eventElem),n.elem[0].eventHandler=!0)},s.that={},s.getThis=function(e){var t=s.that[e];return!t&&n&&layui.hint().error(e?a+" instance with ID '"+e+"' not found":"ID argument required"),t},l.run=function(n){n(r).on("mousedown",function(e){var t,a;!u.thisId||(t=s.getThis(u.thisId))&&(a=t.config,e.target!==a.elem[0]&&e.target!==a.eventElem[0]&&e.target!==n(a.closeStop)[0]&&t.remove())}).on("keydown",function(e){var t;!u.thisId||(t=s.getThis(u.thisId))&&"static"!==t.config.position&&13===e.keyCode&&n("#"+t.elemID)[0]&&t.elemID===g.thisElemDate&&(e.preventDefault(),n(t.footer).find(E)[0].click())}),n(i).on("resize",function(){if(u.thisId){var e=s.getThis(u.thisId);if(e)return!(!e.elem||!n(".layui-laydate")[0])&&void e.position()}})},u.render=function(e){e=new g(e);return s.call(e)},u.parse=function(a,n,i){return a=a||{},n=((n="string"==typeof n?s.formatArr(n):n)||[]).concat(),lay.each(n,function(e,t){/yyyy|y/.test(t)?n[e]=lay.digit(a.year,t.length):/MM|M/.test(t)?n[e]=lay.digit(a.month+(i||0),t.length):/dd|d/.test(t)?n[e]=lay.digit(a.date,t.length):/HH|H/.test(t)?n[e]=lay.digit(a.hours,t.length):/mm|m/.test(t)?n[e]=lay.digit(a.minutes,t.length):/ss|s/.test(t)&&(n[e]=lay.digit(a.seconds,t.length))}),n.join("")},u.getEndDate=function(e,t){var a=new Date;return a.setFullYear(t||a.getFullYear(),e||a.getMonth()+1,1),new Date(a.getTime()-864e5).getDate()},u.close=function(e){e=s.getThis(e||u.thisId);if(e)return e.remove()},n?(u.ready(),layui.define("lay",function(e){u.path=layui.cache.dir,l.run(lay),e(a,u)})):"function"==typeof define&&define.amd?define(function(){return l.run(lay),u}):(u.ready(),l.run(i.lay),i.laydate=u)}(window,window.document);!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e):function(e){if(e.document)return t(e);throw new Error("jQuery requires a window with a document")}:t(e)}("undefined"!=typeof window?window:this,function(T,M){var f=[],g=T.document,c=f.slice,O=f.concat,R=f.push,P=f.indexOf,B={},W=B.toString,m=B.hasOwnProperty,y={},e="1.12.4",C=function(e,t){return new C.fn.init(e,t)},I=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,$=/^-ms-/,z=/-([\da-z])/gi,X=function(e,t){return t.toUpperCase()};function U(e){var t=!!e&&"length"in e&&e.length,n=C.type(e);return"function"!==n&&!C.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+a+")"+a+"*"),ee=new RegExp("="+a+"*([^\\]'\"]*?)"+a+"*\\]","g"),te=new RegExp(G),ne=new RegExp("^"+s+"$"),f={ID:new RegExp("^#("+s+")"),CLASS:new RegExp("^\\.("+s+")"),TAG:new RegExp("^("+s+"|[*])"),ATTR:new RegExp("^"+J),PSEUDO:new RegExp("^"+G),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+a+"*(even|odd|(([+-]|)(\\d*)n|)"+a+"*(?:([+-]|)"+a+"*(\\d+)|))"+a+"*\\)|)","i"),bool:new RegExp("^(?:"+Y+")$","i"),needsContext:new RegExp("^"+a+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+a+"*((?:-\\d)?\\d*)"+a+"*\\)|)(?=[^-]|$)","i")},re=/^(?:input|select|textarea|button)$/i,ie=/^h\d$/i,c=/^[^{]+\{\s*\[native \w/,oe=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ae=/[+~]/,se=/'|\\/g,d=new RegExp("\\\\([\\da-f]{1,6}"+a+"?|("+a+")|.)","ig"),p=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(65536+r):String.fromCharCode(r>>10|55296,1023&r|56320)},ue=function(){C()};try{D.apply(n=V.call(v.childNodes),v.childNodes),n[v.childNodes.length].nodeType}catch(F){D={apply:n.length?function(e,t){U.apply(e,V.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function H(e,t,n,r){var i,o,a,s,u,l,c,f,d=t&&t.ownerDocument,p=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==p&&9!==p&&11!==p)return n;if(!r&&((t?t.ownerDocument||t:v)!==E&&C(t),t=t||E,N)){if(11!==p&&(l=oe.exec(e)))if(i=l[1]){if(9===p){if(!(a=t.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(d&&(a=d.getElementById(i))&&y(t,a)&&a.id===i)return n.push(a),n}else{if(l[2])return D.apply(n,t.getElementsByTagName(e)),n;if((i=l[3])&&g.getElementsByClassName&&t.getElementsByClassName)return D.apply(n,t.getElementsByClassName(i)),n}if(g.qsa&&!A[e+" "]&&(!m||!m.test(e))){if(1!==p)d=t,f=e;else if("object"!==t.nodeName.toLowerCase()){for((s=t.getAttribute("id"))?s=s.replace(se,"\\$&"):t.setAttribute("id",s=k),o=(c=w(e)).length,u=ne.test(s)?"#"+s:"[id='"+s+"']";o--;)c[o]=u+" "+_(c[o]);f=c.join(","),d=ae.test(e)&&de(t.parentNode)||t}if(f)try{return D.apply(n,d.querySelectorAll(f)),n}catch(h){}finally{s===k&&t.removeAttribute("id")}}}return P(e.replace(L,"$1"),t,n,r)}function le(){var n=[];function r(e,t){return n.push(e+" ")>b.cacheLength&&delete r[n.shift()],r[e+" "]=t}return r}function q(e){return e[k]=!0,e}function h(e){var t=E.createElement("div");try{return!!e(t)}catch(F){return!1}finally{t.parentNode&&t.parentNode.removeChild(t)}}function ce(e,t){for(var n=e.split("|"),r=n.length;r--;)b.attrHandle[n[r]]=t}function fe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||1<<31)-(~e.sourceIndex||1<<31);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function x(a){return q(function(o){return o=+o,q(function(e,t){for(var n,r=a([],e.length,o),i=r.length;i--;)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function de(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in g=H.support={},O=H.isXML=function(e){e=e&&(e.ownerDocument||e).documentElement;return!!e&&"HTML"!==e.nodeName},C=H.setDocument=function(e){var e=e?e.ownerDocument||e:v;return e!==E&&9===e.nodeType&&e.documentElement&&(t=(E=e).documentElement,N=!O(E),(e=E.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",ue,!1):e.attachEvent&&e.attachEvent("onunload",ue)),g.attributes=h(function(e){return e.className="i",!e.getAttribute("className")}),g.getElementsByTagName=h(function(e){return e.appendChild(E.createComment("")),!e.getElementsByTagName("*").length}),g.getElementsByClassName=c.test(E.getElementsByClassName),g.getById=h(function(e){return t.appendChild(e).id=k,!E.getElementsByName||!E.getElementsByName(k).length}),g.getById?(b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&N)return(e=t.getElementById(e))?[e]:[]},b.filter.ID=function(e){var t=e.replace(d,p);return function(e){return e.getAttribute("id")===t}}):(delete b.find.ID,b.filter.ID=function(e){var t=e.replace(d,p);return function(e){e="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return e&&e.value===t}}),b.find.TAG=g.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):g.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"!==e)return o;for(;n=o[i++];)1===n.nodeType&&r.push(n);return r},b.find.CLASS=g.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&N)return t.getElementsByClassName(e)},r=[],m=[],(g.qsa=c.test(E.querySelectorAll))&&(h(function(e){t.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+a+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+a+"*(?:value|"+Y+")"),e.querySelectorAll("[id~="+k+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||m.push(".#.+[+~]")}),h(function(e){var t=E.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+a+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(g.matchesSelector=c.test(i=t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.msMatchesSelector))&&h(function(e){g.disconnectedMatch=i.call(e,"div"),i.call(e,"[s!='']:x"),r.push("!=",G)}),m=m.length&&new RegExp(m.join("|")),r=r.length&&new RegExp(r.join("|")),e=c.test(t.compareDocumentPosition),y=e||c.test(t.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,t=t&&t.parentNode;return e===t||!(!t||1!==t.nodeType||!(n.contains?n.contains(t):e.compareDocumentPosition&&16&e.compareDocumentPosition(t)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},$=e?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!g.sortDetached&&t.compareDocumentPosition(e)===n?e===E||e.ownerDocument===v&&y(v,e)?-1:t===E||t.ownerDocument===v&&y(v,t)?1:u?j(u,e)-j(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===E?-1:t===E?1:i?-1:o?1:u?j(u,e)-j(u,t):0;if(i===o)return fe(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?fe(a[r],s[r]):a[r]===v?-1:s[r]===v?1:0}),E},H.matches=function(e,t){return H(e,null,null,t)},H.matchesSelector=function(e,t){if((e.ownerDocument||e)!==E&&C(e),t=t.replace(ee,"='$1']"),g.matchesSelector&&N&&!A[t+" "]&&(!r||!r.test(t))&&(!m||!m.test(t)))try{var n=i.call(e,t);if(n||g.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(F){}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(d,p),e[3]=(e[3]||e[4]||e[5]||"").replace(d,p),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||H.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&H.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return f.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&te.test(n)&&(t=w(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(d,p).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=W[e+" "];return t||(t=new RegExp("(^|"+a+")"+e+"("+a+"|$)"))&&W(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(t,n,r){return function(e){e=H.attr(e,t);return null==e?"!="===n:!n||(e+="","="===n?e===r:"!="===n?e!==r:"^="===n?r&&0===e.indexOf(r):"*="===n?r&&-1(?:<\/\1>|)$/,G=/^.[^:#\[\.,]*$/;function K(e,n,r){if(C.isFunction(n))return C.grep(e,function(e,t){return!!n.call(e,t,e)!==r});if(n.nodeType)return C.grep(e,function(e){return e===n!==r});if("string"==typeof n){if(G.test(n))return C.filter(n,e,r);n=C.filter(n,e)}return C.grep(e,function(e){return-1)[^>]*|#([\w-]*))$/,ee=((C.fn.init=function(e,t,n){if(!e)return this;if(n=n||Q,"string"!=typeof e)return e.nodeType?(this.context=this[0]=e,this.length=1,this):C.isFunction(e)?"undefined"!=typeof n.ready?n.ready(e):e(C):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),C.makeArray(e,this));if(!(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&3<=e.length?[null,e,null]:Z.exec(e))||!r[1]&&t)return(!t||t.jquery?t||n:this.constructor(t)).find(e);if(r[1]){if(t=t instanceof C?t[0]:t,C.merge(this,C.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:g,!0)),J.test(r[1])&&C.isPlainObject(t))for(var r in t)C.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}if((n=g.getElementById(r[2]))&&n.parentNode){if(n.id!==r[2])return Q.find(e);this.length=1,this[0]=n}return this.context=g,this.selector=e,this}).prototype=C.fn,Q=C(g),/^(?:parents|prev(?:Until|All))/),te={children:!0,contents:!0,next:!0,prev:!0};function ne(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}C.fn.extend({has:function(e){var t,n=C(e,this),r=n.length;return this.filter(function(){for(t=0;t
a",y.leadingWhitespace=3===S.firstChild.nodeType,y.tbody=!S.getElementsByTagName("tbody").length,y.htmlSerialize=!!S.getElementsByTagName("link").length,y.html5Clone="<:nav>"!==g.createElement("nav").cloneNode(!0).outerHTML,q.type="checkbox",q.checked=!0,k.appendChild(q),y.appendChecked=q.checked,S.innerHTML="",y.noCloneChecked=!!S.cloneNode(!0).lastChild.defaultValue,k.appendChild(S),(q=g.createElement("input")).setAttribute("type","radio"),q.setAttribute("checked","checked"),q.setAttribute("name","t"),S.appendChild(q),y.checkClone=S.cloneNode(!0).cloneNode(!0).lastChild.checked,y.noCloneEvent=!!S.addEventListener,S[C.expando]=1,y.attributes=!S.getAttribute(C.expando);var x={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:y.htmlSerialize?[0,"",""]:[1,"X
","
"]};function b(e,t){var n,r,i=0,o="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):undefined;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||C.nodeName(r,t)?o.push(r):C.merge(o,b(r,t));return t===undefined||t&&C.nodeName(e,t)?C.merge([e],o):o}function we(e,t){for(var n,r=0;null!=(n=e[r]);r++)C._data(n,"globalEval",!t||C._data(t[r],"globalEval"))}x.optgroup=x.option,x.tbody=x.tfoot=x.colgroup=x.caption=x.thead,x.th=x.td;var Te=/<|&#?\w+;/,Ce=/"!==f[1]||Ce.test(a)?0:u:u.firstChild)&&a.childNodes.length;o--;)C.nodeName(c=a.childNodes[o],"tbody")&&!c.childNodes.length&&a.removeChild(c);for(C.merge(h,u.childNodes),u.textContent="";u.firstChild;)u.removeChild(u.firstChild);u=p.lastChild}else h.push(t.createTextNode(a));for(u&&p.removeChild(u),y.appendChecked||C.grep(b(h,"input"),Ee),g=0;a=h[g++];)if(r&&-1]","i"),Pe=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,Be=/\s*$/g,ze=be(g).appendChild(g.createElement("div"));function Xe(e,t){return C.nodeName(e,"table")&&C.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ue(e){return e.type=(null!==C.find.attr(e,"type"))+"/"+e.type,e}function Ve(e){var t=Ie.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Ye(e,t){if(1===t.nodeType&&C.hasData(e)){var n,r,i,e=C._data(e),o=C._data(t,e),a=e.events;if(a)for(n in delete o.handle,o.events={},a)for(r=0,i=a[n].length;r")},clone:function(e,t,n){var r,i,o,a,s,u=C.contains(e.ownerDocument,e);if(y.html5Clone||C.isXMLDoc(e)||!Re.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(ze.innerHTML=e.outerHTML,ze.removeChild(o=ze.firstChild)),!(y.noCloneEvent&&y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||C.isXMLDoc(e)))for(r=b(o),s=b(e),a=0;null!=(i=s[a]);++a)if(r[a]){f=c=l=p=d=void 0;var l,c,f,d=i,p=r[a];if(1===p.nodeType){if(l=p.nodeName.toLowerCase(),!y.noCloneEvent&&p[C.expando]){for(c in(f=C._data(p)).events)C.removeEvent(p,c,f.handle);p.removeAttribute(C.expando)}"script"===l&&p.text!==d.text?(Ue(p).text=d.text,Ve(p)):"object"===l?(p.parentNode&&(p.outerHTML=d.outerHTML),y.html5Clone&&d.innerHTML&&!C.trim(p.innerHTML)&&(p.innerHTML=d.innerHTML)):"input"===l&&ge.test(d.type)?(p.defaultChecked=p.checked=d.checked,p.value!==d.value&&(p.value=d.value)):"option"===l?p.defaultSelected=p.selected=d.defaultSelected:"input"!==l&&"textarea"!==l||(p.defaultValue=d.defaultValue)}}if(t)if(n)for(s=s||b(e),r=r||b(o),a=0;null!=(i=s[a]);a++)Ye(i,r[a]);else Ye(e,o);return 0<(r=b(o,"script")).length&&we(r,!u&&b(e,"script")),r=s=i=null,o},cleanData:function(e,t){for(var n,r,i,o,a=0,s=C.expando,u=C.cache,l=y.attributes,c=C.event.special;null!=(n=e[a]);a++)if((t||v(n))&&(o=(i=n[s])&&u[i])){if(o.events)for(r in o.events)c[r]?C.event.remove(n,r):C.removeEvent(n,r,o.handle);u[i]&&(delete u[i],l||"undefined"==typeof n.removeAttribute?n[s]=undefined:n.removeAttribute(s),f.push(i))}}}),C.fn.extend({domManip:w,detach:function(e){return Je(this,e,!0)},remove:function(e){return Je(this,e)},text:function(e){return d(this,function(e){return e===undefined?C.text(this):this.empty().append((this[0]&&this[0].ownerDocument||g).createTextNode(e))},null,e,arguments.length)},append:function(){return w(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Xe(this,e).appendChild(e)})},prepend:function(){return w(this,arguments,function(e){var t;1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(t=Xe(this,e)).insertBefore(e,t.firstChild)})},before:function(){return w(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return w(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&C.cleanData(b(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&C.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return C.clone(this,e,t)})},html:function(e){return d(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined)return 1===t.nodeType?t.innerHTML.replace(Oe,""):undefined;if("string"==typeof e&&!Be.test(e)&&(y.htmlSerialize||!Re.test(e))&&(y.leadingWhitespace||!ve.test(e))&&!x[(me.exec(e)||["",""])[1].toLowerCase()]){e=C.htmlPrefilter(e);try{for(;n")).appendTo(t.documentElement))[0].contentWindow||Ge[0].contentDocument).document).write(),t.close(),n=Qe(e,t),Ge.detach()),Ke[e]=n),n}var n,et,tt,nt,rt,it,ot,a,at=/^margin/,st=new RegExp("^("+e+")(?!px)[a-z%]+$","i"),ut=function(e,t,n,r){var i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.apply(e,r||[]),t)e.style[i]=o[i];return r},lt=g.documentElement;function t(){var e,t=g.documentElement;t.appendChild(ot),a.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",n=tt=it=!1,et=rt=!0,T.getComputedStyle&&(e=T.getComputedStyle(a),n="1%"!==(e||{}).top,it="2px"===(e||{}).marginLeft,tt="4px"===(e||{width:"4px"}).width,a.style.marginRight="50%",et="4px"===(e||{marginRight:"4px"}).marginRight,(e=a.appendChild(g.createElement("div"))).style.cssText=a.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",e.style.marginRight=e.style.width="0",a.style.width="1px",rt=!parseFloat((T.getComputedStyle(e)||{}).marginRight),a.removeChild(e)),a.style.display="none",(nt=0===a.getClientRects().length)&&(a.style.display="",a.innerHTML="
t
",a.childNodes[0].style.borderCollapse="separate",(e=a.getElementsByTagName("td"))[0].style.cssText="margin:0;border:0;padding:0;display:none",(nt=0===e[0].offsetHeight)&&(e[0].style.display="",e[1].style.display="none",nt=0===e[0].offsetHeight)),t.removeChild(ot)}ot=g.createElement("div"),(a=g.createElement("div")).style&&(a.style.cssText="float:left;opacity:.5",y.opacity="0.5"===a.style.opacity,y.cssFloat=!!a.style.cssFloat,a.style.backgroundClip="content-box",a.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===a.style.backgroundClip,(ot=g.createElement("div")).style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",a.innerHTML="",ot.appendChild(a),y.boxSizing=""===a.style.boxSizing||""===a.style.MozBoxSizing||""===a.style.WebkitBoxSizing,C.extend(y,{reliableHiddenOffsets:function(){return null==n&&t(),nt},boxSizingReliable:function(){return null==n&&t(),tt},pixelMarginRight:function(){return null==n&&t(),et},pixelPosition:function(){return null==n&&t(),n},reliableMarginRight:function(){return null==n&&t(),rt},reliableMarginLeft:function(){return null==n&&t(),it}}));var l,p,ct=/^(top|right|bottom|left)$/;function ft(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}T.getComputedStyle?(l=function(e){var t=e.ownerDocument.defaultView;return(t=t&&t.opener?t:T).getComputedStyle(e)},p=function(e,t,n){var r,i,o=e.style;return""!==(i=(n=n||l(e))?n.getPropertyValue(t)||n[t]:undefined)&&i!==undefined||C.contains(e.ownerDocument,e)||(i=C.style(e,t)),n&&!y.pixelMarginRight()&&st.test(i)&&at.test(t)&&(e=o.width,t=o.minWidth,r=o.maxWidth,o.minWidth=o.maxWidth=o.width=i,i=n.width,o.width=e,o.minWidth=t,o.maxWidth=r),i===undefined?i:i+""}):lt.currentStyle&&(l=function(e){return e.currentStyle},p=function(e,t,n){var r,i,o,a=e.style;return null==(n=(n=n||l(e))?n[t]:undefined)&&a&&a[t]&&(n=a[t]),st.test(n)&&!ct.test(t)&&(r=a.left,(o=(i=e.runtimeStyle)&&i.left)&&(i.left=e.currentStyle.left),a.left="fontSize"===t?"1em":n,n=a.pixelLeft+"px",a.left=r,o&&(i.left=o)),n===undefined?n:n+""||"auto"});var dt=/alpha\([^)]*\)/i,pt=/opacity\s*=\s*([^)]*)/i,ht=/^(none|table(?!-c[ea]).+)/,gt=new RegExp("^("+e+")(.*)$","i"),mt={position:"absolute",visibility:"hidden",display:"block"},yt={letterSpacing:"0",fontWeight:"400"},vt=["Webkit","O","Moz","ms"],xt=g.createElement("div").style;function bt(e){if(e in xt)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=vt.length;n--;)if((e=vt[n]+t)in xt)return e}function wt(e,t){for(var n,r,i,o=[],a=0,s=e.length;a
a",F=q.getElementsByTagName("a")[0],k.setAttribute("type","checkbox"),q.appendChild(k),(F=q.getElementsByTagName("a")[0]).style.cssText="top:1px",y.getSetAttribute="t"!==q.className,y.style=/top/.test(F.getAttribute("style")),y.hrefNormalized="/a"===F.getAttribute("href"),y.checkOn=!!k.value,y.optSelected=e.selected,y.enctype=!!g.createElement("form").enctype,S.disabled=!0,y.optDisabled=!e.disabled,(k=g.createElement("input")).setAttribute("value",""),y.input=""===k.getAttribute("value"),k.value="t",k.setAttribute("type","radio"),y.radioValue="t"===k.value;var Lt=/\r/g,Ht=/[\x20\t\r\n\f]+/g;C.fn.extend({val:function(t){var n,e,r,i=this[0];return arguments.length?(r=C.isFunction(t),this.each(function(e){1===this.nodeType&&(null==(e=r?t.call(this,e,C(this).val()):t)?e="":"number"==typeof e?e+="":C.isArray(e)&&(e=C.map(e,function(e){return null==e?"":e+""})),(n=C.valHooks[this.type]||C.valHooks[this.nodeName.toLowerCase()])&&"set"in n&&n.set(this,e,"value")!==undefined||(this.value=e))})):i?(n=C.valHooks[i.type]||C.valHooks[i.nodeName.toLowerCase()])&&"get"in n&&(e=n.get(i,"value"))!==undefined?e:"string"==typeof(e=i.value)?e.replace(Lt,""):null==e?"":e:void 0}}),C.extend({valHooks:{option:{get:function(e){var t=C.find.attr(e,"value");return null!=t?t:C.trim(C.text(e)).replace(Ht," ")}},select:{get:function(e){for(var t,n=e.options,r=e.selectedIndex,i="select-one"===e.type||r<0,o=i?null:[],a=i?r+1:n.length,s=r<0?a:i?r:0;s").append(C.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},C.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){C.fn[t]=function(e){return this.on(t,e)}}),C.expr.filters.animated=function(t){return C.grep(C.timers,function(e){return t===e.elem}).length},C.offset={setOffset:function(e,t,n){var r,i,o,a,s=C.css(e,"position"),u=C(e),l={};"static"===s&&(e.style.position="relative"),o=u.offset(),r=C.css(e,"top"),a=C.css(e,"left"),s=("absolute"===s||"fixed"===s)&&-1'+(o?n.title[0]:n.title)+"":"";return n.zIndex=a,t([n.shade?'
':"",'
'+(e&&2!=n.type?"":o)+'
'+(0==n.type&&-1!==n.icon?'':"")+((1!=n.type||!e)&&n.content||"")+'
'+(i=s?'':"",n.closeBtn&&(i+=''),i)+""+(n.btn?function(){var e="";"string"==typeof n.btn&&(n.btn=[n.btn]);for(var t=0,i=n.btn.length;t'+n.btn[t]+"";return'
'+e+"
"}():"")+(n.resize?'':"")+"
"],o,h('
')),this},t.pt.creat=function(){var e,n=this,a=n.config,o=n.index,s="object"==typeof(l=a.content),r=h("body");if(!a.id||!h("#"+a.id)[0]){switch("string"==typeof a.area&&(a.area="auto"===a.area?["",""]:[a.area,""]),a.shift&&(a.anim=a.shift),6==m.ie&&(a.fixed=!1),a.type){case 0:a.btn="btn"in a?a.btn:c.btn[0],m.closeAll("dialog");break;case 2:var l=a.content=s?a.content:[a.content||"","auto"];a.content='';break;case 3:delete a.title,delete a.closeBtn,-1===a.icon&&a.icon,m.closeAll("loading");break;case 4:s||(a.content=[a.content,"body"]),a.follow=a.content[1],a.content=a.content[0]+'',delete a.title,a.tips="object"==typeof a.tips?a.tips:[a.tips,!0],a.tipsMore||m.closeAll("tips")}n.vessel(s,function(e,t,i){r.append(e[0]),s?2==a.type||4==a.type?h("body").append(e[1]):l.parents("."+d[0])[0]||(l.data("display",l.css("display")).show().addClass("layui-layer-wrap").wrap(e[1]),h("#"+d[0]+o).find("."+d[5]).before(t)):r.append(e[1]),h("#"+d.MOVE)[0]||r.append(c.moveElem=i),n.layero=h("#"+d[0]+o),n.shadeo=h("#"+d.SHADE+o),a.scrollbar||d.html.css("overflow","hidden").attr("layer-full",o)}).auto(o),n.shadeo.css({"background-color":a.shade[1]||"#000",opacity:a.shade[0]||a.shade}),2==a.type&&6==m.ie&&n.layero.find("iframe").attr("src",l[0]),4==a.type?n.tips():(n.offset(),parseInt(c.getStyle(document.getElementById(d.MOVE),"z-index"))||(n.layero.css("visibility","hidden"),m.ready(function(){n.offset(),n.layero.css("visibility","visible")}))),a.fixed&&f.on("resize",function(){n.offset(),(/^\d+%$/.test(a.area[0])||/^\d+%$/.test(a.area[1]))&&n.auto(o),4==a.type&&n.tips()}),a.time<=0||setTimeout(function(){m.close(n.index)},a.time),n.move().callback(),d.anim[a.anim]&&(e="layer-anim "+d.anim[a.anim],n.layero.addClass(e).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){h(this).removeClass(e)})),a.isOutAnim&&n.layero.data("isOutAnim",!0)}},t.pt.auto=function(e){var t=this.config,i=h("#"+d[0]+e),n=(""===t.area[0]&&0t.maxWidth&&i.width(t.maxWidth)),[i.innerWidth(),i.innerHeight()]),a=i.find(d[1]).outerHeight()||0,o=i.find("."+d[6]).outerHeight()||0,e=function(e){(e=i.find(e)).height(n[1]-a-o-2*(0|parseFloat(e.css("padding-top"))))};return 2===t.type?e("iframe"):""===t.area[1]?0t.maxHeight?(n[1]=t.maxHeight,e("."+d[5])):t.fixed&&n[1]>=f.height()&&(n[1]=f.height(),e("."+d[5])):e("."+d[5]),this},t.pt.offset=function(){var e=this,t=e.config,i=e.layero,n=[i.outerWidth(),i.outerHeight()],a="object"==typeof t.offset;e.offsetTop=(f.height()-n[1])/2,e.offsetLeft=(f.width()-n[0])/2,a?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=f.width()-n[0]:"b"===t.offset?e.offsetTop=f.height()-n[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=f.height()-n[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=f.width()-n[0]):"rb"===t.offset?(e.offsetTop=f.height()-n[1],e.offsetLeft=f.width()-n[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?f.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?f.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=f.scrollTop(),e.offsetLeft+=f.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=f.height()-(i.find(d[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},t.pt.tips=function(){var e=this.config,t=this.layero,i=[t.outerWidth(),t.outerHeight()],n=h(e.follow),a={width:(n=n[0]?n:h("body")).outerWidth(),height:n.outerHeight(),top:n.offset().top,left:n.offset().left},o=t.find(".layui-layer-TipsG"),n=e.tips[0];e.tips[1]||o.remove(),a.autoLeft=function(){0":'',o=i.success;return delete i.success,m.open(h.extend({type:1,btn:["确定","取消"],content:t,skin:"layui-layer-prompt"+g("prompt"),maxWidth:f.width(),success:function(e){(a=e.find(".layui-layer-input")).val(i.value||"").focus(),"function"==typeof o&&o(e)},resize:!1,yes:function(e){var t=a.val();""===t?a.focus():t.length>(i.maxlength||500)?m.tips("最多输入"+(i.maxlength||500)+"个字数",a,{tips:1}):n&&n(t,e,a)}},i))},m.tab=function(n){var a=(n=n||{}).tab||{},o="layui-this",s=n.success;return delete n.success,m.open(h.extend({type:1,skin:"layui-layer-tab"+g("tab"),resize:!1,title:function(){var e=a.length,t=1,i="";if(0'+a[0].title+"";t"+a[t].title+"";return i}(),content:'
    '+function(){var e=a.length,t=1,i="";if(0'+(a[0].content||"no content")+"";t'+(a[t].content||"no content")+"";return i}()+"
",success:function(e){var t=e.find(".layui-layer-title").children(),i=e.find(".layui-layer-tabmain").children();t.on("mousedown",function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0;var e=h(this),t=e.index();e.addClass(o).siblings().removeClass(o),i.eq(t).show().siblings().hide(),"function"==typeof n.change&&n.change(t)}),"function"==typeof s&&s(e)}},n))},m.photos=function(i,e,n){var a={};if((i=i||{}).photos){var t=!("string"==typeof i.photos||i.photos instanceof h),o=t?i.photos:{},s=o.data||[],r=o.start||0,l=(a.imgIndex=1+(0|r),i.img=i.img||"img",i.success);if(delete i.success,t){if(0===s.length)return m.msg("没有图片")}else{var f=h(i.photos),c=function(){s=[],f.find(i.img).each(function(e){var t=h(this);t.attr("layer-index",e),s.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(c(),0===s.length)return;if(e||f.on("click",i.img,function(){c();var e=h(this).attr("layer-index");m.photos(h.extend(i,{photos:{start:e,data:s,tab:i.tab},full:i.full}),!0)}),!e)return}a.imgprev=function(e){a.imgIndex--,a.imgIndex<1&&(a.imgIndex=s.length),a.tabimg(e)},a.imgnext=function(e,t){a.imgIndex++,a.imgIndex>s.length&&(a.imgIndex=1,t)||a.tabimg(e)},a.keyup=function(e){var t;a.end||(t=e.keyCode,e.preventDefault(),37===t?a.imgprev(!0):39===t?a.imgnext(!0):27===t&&m.close(a.index))},a.tabimg=function(e){if(!(s.length<=1))return o.start=a.imgIndex-1,m.close(a.index),m.photos(i,!0,e)},a.event=function(){a.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),a.imgprev(!0)}),a.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),a.imgnext(!0)}),h(document).on("keyup",a.keyup)},a.loadi=m.load(1,{shade:!("shade"in i)&&.9,scrollbar:!1});var t=s[r].src,d=function(e){var t;m.close(a.loadi),n&&(i.anim=-1),a.index=m.open(h.extend({type:1,id:"layui-layer-photos",area:(e=[e.width,e.height],t=[h(p).width()-100,h(p).height()-100],!i.full&&(e[0]>t[0]||e[1]>t[1])&&((t=[e[0]/t[0],e[1]/t[1]])[1]'+(s[r].alt||'+(1
'+(s[r].alt||"")+""+a.imgIndex+" / "+s.length+"
":"")+"",success:function(e,t){a.bigimg=e.find(".layui-layer-phimg"),a.imgsee=e.find(".layui-layer-imgbar"),a.event(e),i.tab&&i.tab(s[r],e),"function"==typeof l&&l(e)},end:function(){a.end=!0,h(document).off("keyup",a.keyup)}},i))},u=function(){m.close(a.loadi),m.msg("当前图片地址异常
是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){1',t.bar1?'
  • '+l[0]+"
  • ":"",t.bar2?'
  • '+l[1]+"
  • ":"",'
  • '+l[2]+"
  • ",""].join("")),c=l.find("."+o),g=function(){a.scrollTop()>=t.showHeight?e||(c.show(),e=1):e&&(c.hide(),e=0)};u("."+n)[0]||("object"==typeof t.css&&l.css(t.css),r.append(l),g(),l.find("li").on("click",function(){var e=u(this).attr("lay-type");"top"===e&&u("html,body").animate({scrollTop:0},200),t.click&&t.click.call(this,e)}),a.on("scroll",function(){clearTimeout(i),i=setTimeout(function(){g()},100)}))},countdown:function(e,t,i){var n=this,o="function"==typeof t,a=new Date(e).getTime(),r=new Date(!t||o?(new Date).getTime():t).getTime(),a=a-r,l=[Math.floor(a/864e5),Math.floor(a/36e5)%24,Math.floor(a/6e4)%60,Math.floor(a/1e3)%60],o=(o&&(i=t),setTimeout(function(){n.countdown(e,r+1e3,i)},1e3));return i&&i(0]|&(?=#[a-zA-Z0-9]+)/g.test(e+="")?e.replace(/&(?!#?[a-zA-Z0-9]+;)/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,"""):e},unescape:function(e){return e!==undefined&&null!==e||(e=""),(e+="").replace(/\&/g,"&").replace(/\</g,"<").replace(/\>/g,">").replace(/\'/g,"'").replace(/\"/g,'"')},toVisibleArea:function(e){var t,i,n,o,a,r,l,c;(e=u.extend({margin:160,duration:200,type:"y"},e)).scrollElem[0]&&e.thisElem[0]&&(t=e.scrollElem,l=e.thisElem,n=(a="y"===e.type)?"top":"left",o=t[i=a?"scrollTop":"scrollLeft"](),a=t[a?"height":"width"](),r=t.offset()[n],c={},((l=l.offset()[n]-r)>a-e.margin||l."+y,k=function(e){var i=this;i.index=++c.index,i.config=s.extend({},i.config,c.config,e),i.init()};k.prototype.config={trigger:"click",content:"",className:"",style:"",show:!1,isAllowSpread:!0,isSpreadItem:!0,data:[],delay:300},k.prototype.reload=function(e){var i=this;i.config=s.extend({},i.config,e),i.init(!0)},k.prototype.init=function(e){var i=this,t=i.config,n=t.elem=s(t.elem);return 1",(t="href"in i?''+l+"":l,n?'
    '+t+("parent"===o?'':"group"===o&&u.isAllowSpread?'':"")+"
    ":'
    '+t+"
    "),""].join(""))).data("item",i),n&&(a=s('
    '),t=s("
      "),"parent"===o?(a.append(d(t,i.child)),l.append(a)):l.append(d(t,i.child))),r.append(l))}),r},t=['
      ',"
      "].join("");!(e="contextmenu"!==u.trigger&&!lay.isTopElem(u.elem[0])?e:!0)&&u.elem.data(r+"_opened")||(n.elemView=s(t),n.elemView.append(u.content||(e=s('
        '),0no menu'),e)),u.className&&n.elemView.addClass(u.className),u.style&&n.elemView.attr("style",u.style),c.thisId=u.id,n.remove(),i.append(n.elemView),u.elem.data(r+"_opened",!0),n.position(),(p.prevElem=n.elemView).data("prevElem",u.elem),n.elemView.find(".layui-menu").on(l,function(e){layui.stope(e)}),n.elemView.find(".layui-menu li").on("click",function(e){var i=s(this),t=i.data("item")||{};t.child&&0n.width()&&(t.addClass(C),(i=t[0].getBoundingClientRect()).left<0&&t.removeClass(C)),i.bottom>n.height()&&t.eq(0).css("margin-top",-(i.bottom-n.height()+5)))}).on("mouseleave",t,function(e){var i=s(this).children("."+w);i.removeClass(C),i.css("margin-top",0)}),c.reload=function(e,i){e=p.getThis(e);return e?(e.reload(i),p.call(e)):this},c.render=function(e){e=new k(e);return p.call(e)},e(o,c)});layui.define("jquery",function(e){"use strict";var h=layui.$,t={config:{},index:layui.slider?layui.slider.index+1e4:0,set:function(e){var i=this;return i.config=h.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,a,e,i)}},a="slider",c="layui-disabled",y="layui-slider-bar",g="layui-slider-wrap",b="layui-slider-wrap-btn",x="layui-slider-tips",T="layui-slider-input-txt",w="layui-slider-hover",i=function(e){var i=this;i.index=++t.index,i.config=h.extend({},i.config,t.config,e),i.render()};i.prototype.config={type:"default",min:0,max:100,value:0,step:1,showstep:!1,tips:!0,input:!1,range:!1,height:200,disabled:!1,theme:"#009688"},i.prototype.render=function(){var a,n=this,l=n.config,e=(l.step<1&&(l.step=1),l.maxl.min?i:l.min,l.value[1]=s>l.min?s:l.min,l.value[0]=l.value[0]>l.max?l.max:l.value[0],l.value[1]=l.value[1]>l.max?l.max:l.value[1],i=Math.floor((l.value[0]-l.min)/(l.max-l.min)*100),t=(s=Math.floor((l.value[1]-l.min)/(l.max-l.min)*100))-i+"%",i+="%",s+="%"):("object"==typeof l.value&&(l.value=Math.min.apply(null,l.value)),l.valuel.max&&(l.value=l.max),t=Math.floor((l.value-l.min)/(l.max-l.min)*100)+"%"),l.disabled?"#c2c2c2":l.theme),i='
        '+(l.tips?'
        ':"")+'
        '+(l.range?'
        ':"")+"
        ",t=h(l.elem),s=t.next(".layui-slider");if(s[0]&&s.remove(),n.elemTemp=h(i),l.range?(n.elemTemp.find("."+g).eq(0).data("value",l.value[0]),n.elemTemp.find("."+g).eq(1).data("value",l.value[1])):n.elemTemp.find("."+g).data("value",l.value),t.html(n.elemTemp),"vertical"===l.type&&n.elemTemp.height(l.height+"px"),l.showstep){for(var o=(l.max-l.min)/l.step,r="",u=1;u<1+o;u++){var d=100*u/o;d<100&&(r+='
        ')}n.elemTemp.append(r)}l.input&&!l.range&&(e=h('
        '),t.css("position","relative"),t.append(e),t.find("."+T).children("input").val(l.value),"vertical"===l.type?e.css({left:0,top:-48}):n.elemTemp.css("margin-right",e.outerWidth()+15)),l.disabled?(n.elemTemp.addClass(c),n.elemTemp.find("."+b).addClass(c)):n.slide(),n.elemTemp.find("."+b).on("mouseover",function(){var e="vertical"===l.type?l.height:n.elemTemp[0].offsetWidth,i=n.elemTemp.find("."+g),t=("vertical"===l.type?e-h(this).parent()[0].offsetTop-i.height():h(this).parent()[0].offsetLeft)/e*100,i=h(this).parent().data("value"),e=l.setTips?l.setTips(i):i;n.elemTemp.find("."+x).html(e),clearTimeout(a),a=setTimeout(function(){"vertical"===l.type?n.elemTemp.find("."+x).css({bottom:t+"%","margin-bottom":"20px",display:"inline-block"}):n.elemTemp.find("."+x).css({left:t+"%",display:"inline-block"})},300)}).on("mouseout",function(){clearTimeout(a),n.elemTemp.find("."+x).css("display","none")})},i.prototype.slide=function(e,i,t){var o=this.config,r=this.elemTemp,u=function(){return"vertical"===o.type?o.height:r[0].offsetWidth},d=r.find("."+g),s=r.next(".layui-slider-input"),c=s.children("."+T).children("input").val(),m=100/((o.max-o.min)/Math.ceil(o.step)),v=function(e,i){e=100<(e=100t[1]&&t.reverse(),o.change&&o.change(o.range?t:n)},p=function(e){var i=e/u()*100/m,t=Math.round(i)*m;return t=e==u()?Math.ceil(i)*m:t},f=h(['
        u()?u():i)/u()*100/m;v(i,l),s.addClass(w),r.find("."+x).show(),e.preventDefault()},i=function(){s.removeClass(w),r.find("."+x).hide()},t=function(){i&&i(),f.remove()},h("#LAY-slider-moving")[0]||h("body").append(f),f.on("mousemove",e),f.on("mouseup",t).on("mouseleave",t)})}),r.on("click",function(e){var i=h("."+b),t=h(this);!i.is(event.target)&&0===i.has(event.target).length&&i.length&&(t=(i=(i=(i="vertical"===o.type?u()-e.clientY+t.offset().top-h(window).scrollTop():e.clientX-t.offset().left-h(window).scrollLeft())<0?0:i)>u()?u():i)/u()*100/m,i=o.range?"vertical"===o.type?Math.abs(i-parseInt(h(d[0]).css("bottom")))>Math.abs(i-parseInt(h(d[1]).css("bottom")))?1:0:Math.abs(i-d[0].offsetLeft)>Math.abs(i-d[1].offsetLeft)?1:0:0,v(t,i),e.preventDefault())}),s.children(".layui-slider-input-btn").children("i").each(function(i){h(this).on("click",function(){c=s.children("."+T).children("input").val();var e=((c=1==i?c-o.stepo.max?o.max:Number(c)+o.step)-o.min)/(o.max-o.min)*100/m;v(e,0)})});var a=function(){var e=this.value,e=(e=(e=(e=isNaN(e)?0:e)o.max?o.max:e,((this.value=e)-o.min)/(o.max-o.min)*100/m);v(e,0)};s.children("."+T).children("input").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),a.call(this))}).on("change",a)},i.prototype.events=function(){this.config},t.render=function(e){e=new i(e);return function(){var t=this,a=t.config;return{setValue:function(e,i){return a.value=e,t.slide("set",e,i||0)},config:a}}.call(e)},e(a,t)});layui.define(["jquery","lay"],function(e){"use strict";var k=layui.jquery,n=layui.lay,r=layui.device().mobile?"click":"mousedown",l={config:{},index:layui.colorpicker?layui.colorpicker.index+1e4:0,set:function(e){var i=this;return i.config=k.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,"colorpicker",e,i)}},t="layui-colorpicker",c=".layui-colorpicker-main",y="layui-icon-down",x="layui-icon-close",P="layui-colorpicker-trigger-span",C="layui-colorpicker-trigger-i",B="layui-colorpicker-side-slider",w="layui-colorpicker-basis",D="layui-colorpicker-alpha-bgcolor",j="layui-colorpicker-alpha-slider",E="layui-colorpicker-basis-cursor",F="layui-colorpicker-main-input",H=function(e){var i={h:0,s:0,b:0},o=Math.min(e.r,e.g,e.b),r=Math.max(e.r,e.g,e.b),n=r-o;return i.b=r,i.s=0!=r?255*n/r:0,0!=i.s?e.r==r?i.h=(e.g-e.b)/n:e.g==r?i.h=2+(e.b-e.r)/n:i.h=4+(e.r-e.g)/n:i.h=-1,r==o&&(i.h=0),i.h*=60,i.h<0&&(i.h+=360),i.s*=100/255,i.b*=100/255,i},M=function(e){var i,o={},r=e.h,n=255*e.s/100,e=255*e.b/100;return 0==n?o.r=o.g=o.b=e:(e=r%60*((i=e)-(n=(255-n)*e/255))/60,(r=360==r?0:r)<60?(o.r=i,o.b=n,o.g=n+e):r<120?(o.g=i,o.b=n,o.r=i-e):r<180?(o.g=i,o.r=n,o.b=n+e):r<240?(o.b=i,o.r=n,o.g=i-e):r<300?(o.b=i,o.g=n,o.r=n+e):r<360?(o.r=i,o.g=n,o.b=i-e):(o.r=0,o.g=0,o.b=0)),{r:Math.round(o.r),g:Math.round(o.g),b:Math.round(o.b)}},f=function(e){var e=M(e),o=[e.r.toString(16),e.g.toString(16),e.b.toString(16)];return k.each(o,function(e,i){1==i.length&&(o[e]="0"+i)}),o.join("")},Y=function(e){e=e.match(/[0-9]{1,3}/g)||[];return{r:e[0],g:e[1],b:e[2]}},I=k(window),a=k(document),s=function(e){this.index=++l.index,this.config=k.extend({},this.config,l.config,e),this.render()};s.prototype.config={color:"",size:null,alpha:!1,format:"hex",predefine:!1,colors:["#009688","#5FB878","#1E9FFF","#FF5722","#FFB800","#01AAED","#999","#c00","#ff8c00","#ffd700","#90ee90","#00ced1","#1e90ff","#c71585","rgb(0, 186, 189)","rgb(255, 120, 0)","rgb(250, 212, 0)","#393D49","rgba(0,0,0,.5)","rgba(255, 69, 0, 0.68)","rgba(144, 240, 144, 0.5)","rgba(31, 147, 255, 0.73)"]},s.prototype.render=function(){var e=this,i=e.config,o=k(i.elem);if(1',"",'','',"","","
        "].join("")),r=i.elem=k(i.elem);i.size&&o.addClass("layui-colorpicker-"+i.size),r.addClass("layui-inline").html(e.elemColorBox=o),e.color=e.elemColorBox.find("."+P)[0].style.background,e.events()},s.prototype.renderPicker=function(){var o,e=this,i=e.config,r=e.elemColorBox[0],i=e.elemPicker=k(['
        ','
        ','
        ','
        ','
        ','
        ',"
        ",'
        ','
        ',"
        ","
        ",'
        ','
        ','
        ',"
        ","
        ",i.predefine?(o=['
        '],layui.each(i.colors,function(e,i){o.push(['
        ','
        ',"
        "].join(""))}),o.push("
        "),o.join("")):"",'
        ','
        ','',"
        ",'
        ','','',"","
        "].join(""));e.elemColorBox.find("."+P)[0];k(c)[0]&&k(c).data("index")==e.index?e.removePicker(s.thisElemInd):(e.removePicker(s.thisElemInd),k("body").append(i)),s.thisElemInd=e.index,s.thisColor=r.style.background,e.position(),e.pickerEvents()},s.prototype.removePicker=function(e){this.config;return k("#layui-colorpicker"+(e||this.index)).remove(),this},s.prototype.position=function(){var e=this,i=e.config;return n.position(e.bindElem||e.elemColorBox[0],e.elemPicker[0],{position:i.position,align:"center"}),e},s.prototype.val=function(){var e,i=this,o=(i.config,i.elemColorBox.find("."+P)),r=i.elemPicker.find("."+F),n=o[0].style.backgroundColor;n?(e=H(Y(n)),o=o.attr("lay-type"),i.select(e.h,e.s,e.b),"torgb"===o&&r.find("input").val(n),"rgba"===o&&(e=Y(n),3==(n.match(/[0-9]{1,3}/g)||[]).length?(r.find("input").val("rgba("+e.r+", "+e.g+", "+e.b+", 1)"),i.elemPicker.find("."+j).css("left",280)):(r.find("input").val(n),o=280*n.slice(n.lastIndexOf(",")+1,n.length-1),i.elemPicker.find("."+j).css("left",o)),i.elemPicker.find("."+D)[0].style.background="linear-gradient(to right, rgba("+e.r+", "+e.g+", "+e.b+", 0), rgb("+e.r+", "+e.g+", "+e.b+"))")):(i.select(0,100,100),r.find("input").val(""),i.elemPicker.find("."+D)[0].style.background="",i.elemPicker.find("."+j).css("left",280))},s.prototype.side=function(){var n=this,l=n.config,t=n.elemColorBox.find("."+P),c=t.attr("lay-type"),a=n.elemPicker.find(".layui-colorpicker-side"),e=n.elemPicker.find("."+B),s=n.elemPicker.find("."+w),r=n.elemPicker.find("."+E),d=n.elemPicker.find("."+D),f=n.elemPicker.find("."+j),u=e[0].offsetTop/180*360,p=100-(r[0].offsetTop+3)/180*100,g=(r[0].offsetLeft+3)/260*100,h=Math.round(f[0].offsetLeft/280*100)/100,v=n.elemColorBox.find("."+C),i=n.elemPicker.find(".layui-colorpicker-pre").children("div"),b=function(e,i,o,r){n.select(e,i,o);e=M({h:e,s:i,b:o});v.addClass(y).removeClass(x),t[0].style.background="rgb("+e.r+", "+e.g+", "+e.b+")","torgb"===c&&n.elemPicker.find("."+F).find("input").val("rgb("+e.r+", "+e.g+", "+e.b+")"),"rgba"===c&&(f.css("left",280*r),n.elemPicker.find("."+F).find("input").val("rgba("+e.r+", "+e.g+", "+e.b+", "+r+")"),t[0].style.background="rgba("+e.r+", "+e.g+", "+e.b+", "+r+")",d[0].style.background="linear-gradient(to right, rgba("+e.r+", "+e.g+", "+e.b+", 0), rgb("+e.r+", "+e.g+", "+e.b+"))"),l.change&&l.change(n.elemPicker.find("."+F).find("input").val())},o=k(['
        '].join("")),m=function(e){k("#LAY-colorpicker-moving")[0]||k("body").append(o),o.on("mousemove",e),o.on("mouseup",function(){o.remove()}).on("mouseleave",function(){o.remove()})};e.on("mousedown",function(e){var r=this.offsetTop,n=e.clientY;m(function(e){var i=r+(e.clientY-n),o=a[0].offsetHeight,o=(i=o<(i=i<0?0:i)?o:i)/180*360;b(u=o,g,p,h),e.preventDefault()}),e.preventDefault()}),a.on("click",function(e){var i=e.clientY-k(this).offset().top,i=(i=(i=i<0?0:i)>this.offsetHeight?this.offsetHeight:i)/180*360;b(u=i,g,p,h),e.preventDefault()}),r.on("mousedown",function(e){var l=this.offsetTop,t=this.offsetLeft,c=e.clientY,a=e.clientX;layui.stope(e),m(function(e){var i=l+(e.clientY-c),o=t+(e.clientX-a),r=s[0].offsetHeight-3,n=s[0].offsetWidth-3,n=((o=n<(o=o<-3?-3:o)?n:o)+3)/260*100,o=100-((i=r<(i=i<-3?-3:i)?r:i)+3)/180*100;b(u,g=n,p=o,h),e.preventDefault()}),e.preventDefault()}),s.on("mousedown",function(e){var i=e.clientY-k(this).offset().top-3+I.scrollTop(),o=e.clientX-k(this).offset().left-3+I.scrollLeft(),o=((i=i<-3?-3:i)>this.offsetHeight-3&&(i=this.offsetHeight-3),((o=(o=o<-3?-3:o)>this.offsetWidth-3?this.offsetWidth-3:o)+3)/260*100),i=100-(i+3)/180*100;b(u,g=o,p=i,h),layui.stope(e),e.preventDefault(),r.trigger(e,"mousedown")}),f.on("mousedown",function(e){var r=this.offsetLeft,n=e.clientX;m(function(e){var i=r+(e.clientX-n),o=d[0].offsetWidth,o=(o<(i=i<0?0:i)&&(i=o),Math.round(i/280*100)/100);b(u,g,p,h=o),e.preventDefault()}),e.preventDefault()}),d.on("click",function(e){var i=e.clientX-k(this).offset().left,i=((i=i<0?0:i)>this.offsetWidth&&(i=this.offsetWidth),Math.round(i/280*100)/100);b(u,g,p,h=i),e.preventDefault()}),i.each(function(){k(this).on("click",function(){k(this).parent(".layui-colorpicker-pre").addClass("selected").siblings().removeClass("selected");var e=this.style.backgroundColor,i=H(Y(e)),o=e.slice(e.lastIndexOf(",")+1,e.length-1);u=i.h,g=i.s,p=i.b,3==(e.match(/[0-9]{1,3}/g)||[]).length&&(o=1),h=o,b(i.h,i.s,i.b,o)})})},s.prototype.select=function(e,i,o,r){var n=this,l=(n.config,f({h:e,s:100,b:100})),t=f({h:e,s:i,b:o}),e=e/360*180,o=180-o/100*180-3,i=i/100*260-3;n.elemPicker.find("."+B).css("top",e),n.elemPicker.find("."+w)[0].style.background="#"+l,n.elemPicker.find("."+E).css({top:o,left:i}),"change"!==r&&n.elemPicker.find("."+F).find("input").val("#"+t)},s.prototype.pickerEvents=function(){var c=this,a=c.config,s=c.elemColorBox.find("."+P),d=c.elemPicker.find("."+F+" input"),o={clear:function(e){s[0].style.background="",c.elemColorBox.find("."+C).removeClass(y).addClass(x),c.color="",a.done&&a.done(""),c.removePicker()},confirm:function(e,i){var o,r,n=d.val(),l=n,t={};if(-1>16,g:(65280&o)>>8,b:255&o},t=H(r),s[0].style.background=l="#"+f(t),c.elemColorBox.find("."+C).removeClass(x).addClass(y)),"change"===i)return c.select(t.h,t.s,t.b,i),void(a.change&&a.change(l));c.color=n,a.done&&a.done(n),c.removePicker()}};c.elemPicker.on("click","*[colorpicker-events]",function(){var e=k(this),i=e.attr("colorpicker-events");o[i]&&o[i].call(this,e)}),d.on("keyup",function(e){var i=k(this);o.confirm.call(this,i,13===e.keyCode?null:"change")})},s.prototype.events=function(){var i=this,e=i.config,o=i.elemColorBox.find("."+P);i.elemColorBox.on("click",function(){i.renderPicker(),k(c)[0]&&(i.val(),i.side())}),e.elem[0]&&!i.elemColorBox[0].eventHandler&&(a.on(r,function(e){k(e.target).hasClass(t)||k(e.target).parents("."+t)[0]||k(e.target).hasClass(c.replace(/\./g,""))||k(e.target).parents(c)[0]||i.elemPicker&&(i.color?(e=H(Y(i.color)),i.select(e.h,e.s,e.b)):i.elemColorBox.find("."+C).removeClass(y).addClass(x),o[0].style.background=i.color||"",i.removePicker())}),I.on("resize",function(){if(!i.elemPicker||!k(c)[0])return!1;i.position()}),i.elemColorBox[0].eventHandler=!0)},l.render=function(e){e=new s(e);return function(){return{config:this.config}}.call(e)},e("colorpicker",l)});layui.define("jquery",function(t){"use strict";var u=layui.$,d=(layui.hint(),layui.device()),c="element",r="layui-this",y="layui-show",i=function(){this.config={}},h=(i.prototype.set=function(t){return u.extend(!0,this.config,t),this},i.prototype.on=function(t,i){return layui.onevent.call(this,c,t,i)},i.prototype.tabAdd=function(t,i){var a,t=u(".layui-tab[lay-filter="+t+"]"),e=t.children(".layui-tab-title"),l=e.children(".layui-tab-bar"),t=t.children(".layui-tab-content"),n=""+(i.title||"unnaming")+"";return l[0]?l.before(n):e.append(n),t.append('
        '+(i.content||"")+"
        "),C.hideTabMore(!0),C.tabAuto(),this},i.prototype.tabDelete=function(t,i){t=u(".layui-tab[lay-filter="+t+"]").children(".layui-tab-title").find('>li[lay-id="'+i+'"]');return C.tabDelete(null,t),this},i.prototype.tabChange=function(t,i){t=u(".layui-tab[lay-filter="+t+"]").children(".layui-tab-title").find('>li[lay-id="'+i+'"]');return C.tabClick.call(t[0],null,null,t),this},i.prototype.tab=function(a){a=a||{},e.on("click",a.headerElem,function(t){var i=u(this).index();C.tabClick.call(this,t,i,null,a)})},i.prototype.progress=function(t,i){var a="layui-progress",t=u("."+a+"[lay-filter="+t+"]").find("."+a+"-bar"),a=t.find("."+a+"-text");return t.css("width",i).attr("lay-percent",i),a.text(i),this},".layui-nav"),f="layui-nav-item",l="layui-nav-bar",p="layui-nav-tree",b="layui-nav-child",v="layui-nav-more",m="layui-anim layui-anim-upbit",C={tabClick:function(t,i,a,e){e=e||{};var a=a||u(this),i=i||a.parent().children("li").index(a),l=e.headerElem?a.parent():a.parents(".layui-tab").eq(0),e=e.bodyElem?u(e.bodyElem):l.children(".layui-tab-content").children(".layui-tab-item"),n=a.find("a"),n="javascript:;"!==n.attr("href")&&"_blank"===n.attr("target"),s="string"==typeof a.attr("lay-unselect"),o=l.attr("lay-filter");n||s||(a.addClass(r).siblings().removeClass(r),e.eq(i).addClass(y).siblings().removeClass(y)),layui.event.call(this,c,"tab("+o+")",{elem:l,index:i})},tabDelete:function(t,i){var i=i||u(this).parent(),a=i.index(),e=i.parents(".layui-tab").eq(0),l=e.children(".layui-tab-content").children(".layui-tab-item"),n=e.attr("lay-filter");i.hasClass(r)&&(i.next()[0]&&i.next().is("li")?C.tabClick.call(i.next()[0],null,a+1):i.prev()[0]&&i.prev().is("li")&&C.tabClick.call(i.prev()[0],null,a-1)),i.remove(),l.eq(a).remove(),setTimeout(function(){C.tabAuto()},50),layui.event.call(this,c,"tabDelete("+n+")",{elem:e,index:a})},tabAuto:function(){var e="layui-tab-bar",l="layui-tab-close",n=this;u(".layui-tab").each(function(){var t=u(this),i=t.children(".layui-tab-title"),a=(t.children(".layui-tab-content").children(".layui-tab-item"),'lay-stope="tabmore"'),a=u('');n===window&&8!=d.ie&&C.hideTabMore(!0),t.attr("lay-allowClose")&&i.find("li").each(function(){var t,i=u(this);i.find("."+l)[0]||((t=u('')).on("click",C.tabDelete),i.append(t))}),"string"!=typeof t.attr("lay-unauto")&&(i.prop("scrollWidth")>i.outerWidth()+1?i.find("."+e)[0]||(i.append(a),t.attr("overflow",""),a.on("click",function(t){i[this.title?"removeClass":"addClass"]("layui-tab-more"),this.title=this.title?"":"\u6536\u7f29"})):(i.find("."+e).remove(),t.removeAttr("overflow")))})},hideTabMore:function(t){var i=u(".layui-tab-title");!0!==t&&"tabmore"===u(t.target).attr("lay-stope")||(i.removeClass("layui-tab-more"),i.find(".layui-tab-bar").attr("title",""))},clickThis:function(){var t=u(this),i=t.parents(h),a=i.attr("lay-filter"),e=t.parent(),l=t.siblings("."+b),n="string"==typeof e.attr("lay-unselect");"javascript:;"!==t.attr("href")&&"_blank"===t.attr("target")||n||l[0]||(i.find("."+r).removeClass(r),e.addClass(r)),i.hasClass(p)&&(l.removeClass(m),l[0]&&(e["none"===l.css("display")?"addClass":"removeClass"](f+"ed"),"all"===i.attr("lay-shrink")&&e.siblings().removeClass(f+"ed"))),layui.event.call(this,c,"nav("+a+")",t)},collapse:function(){var t=u(this),i=t.find(".layui-colla-icon"),a=t.siblings(".layui-colla-content"),e=t.parents(".layui-collapse").eq(0),l=e.attr("lay-filter"),n="none"===a.css("display");"string"==typeof e.attr("lay-accordion")&&((e=e.children(".layui-colla-item").children("."+y)).siblings(".layui-colla-title").children(".layui-colla-icon").html(""),e.removeClass(y)),a[n?"addClass":"removeClass"](y),i.html(n?"":""),layui.event.call(this,c,"collapse("+l+")",{title:t,content:a,show:n})}},a=(i.prototype.render=i.prototype.init=function(t,i){var a=i?'[lay-filter="'+i+'"]':"",i={tab:function(){C.tabAuto.call({})},nav:function(){var s={},o={},c={},r="layui-nav-title";u(h+a).each(function(t){var i=u(this),a=u(''),e=i.find("."+f);i.find("."+l)[0]||(i.append(a),(i.hasClass(p)?e.find("dd,>."+r):e).on("mouseenter",function(){!function(t,i,a){var e,l=u(this),n=l.find("."+b);i.hasClass(p)?n[0]||(e=l.children("."+r),t.css({top:l.offset().top-i.offset().top,height:(e[0]?e:l).outerHeight(),opacity:1})):(n.addClass(m),n.hasClass("layui-nav-child-c")&&n.css({left:-(n.outerWidth()-l.width())/2}),n[0]?t.css({left:t.position().left+t.width()/2,width:0,opacity:0}):t.css({left:l.position().left+parseFloat(l.css("marginLeft")),top:l.position().top+l.height()-t.height()}),s[a]=setTimeout(function(){t.css({width:n[0]?0:l.width(),opacity:n[0]?0:1})},d.ie&&d.ie<10?0:200),clearTimeout(c[a]),"block"===n.css("display")&&clearTimeout(o[a]),o[a]=setTimeout(function(){n.addClass(y),l.find("."+v).addClass(v+"d")},300))}.call(this,a,i,t)}).on("mouseleave",function(){i.hasClass(p)?a.css({height:0,opacity:0}):(clearTimeout(o[t]),o[t]=setTimeout(function(){i.find("."+b).removeClass(y),i.find("."+v).removeClass(v+"d")},300))}),i.on("mouseleave",function(){clearTimeout(s[t]),c[t]=setTimeout(function(){i.hasClass(p)||a.css({width:0,left:a.position().left+a.width()/2,opacity:0})},200)})),e.find("a").each(function(){var t=u(this);t.parent();t.siblings("."+b)[0]&&!t.children("."+v)[0]&&t.append(''),t.off("click",C.clickThis).on("click",C.clickThis)})})},breadcrumb:function(){u(".layui-breadcrumb"+a).each(function(){var t=u(this),i="lay-separator",a=t.attr(i)||"/",e=t.find("a");e.next("span["+i+"]")[0]||(e.each(function(t){t!==e.length-1&&u(this).after(""+a+"")}),t.css("visibility","visible"))})},progress:function(){var e="layui-progress";u("."+e+a).each(function(){var t=u(this),i=t.find(".layui-progress-bar"),a=i.attr("lay-percent");i.css("width",/^.+\/.+$/.test(a)?100*new Function("return "+a)()+"%":a),t.attr("lay-showPercent")&&setTimeout(function(){i.html(''+a+"")},350)})},collapse:function(){u(".layui-collapse"+a).each(function(){u(this).find(".layui-colla-item").each(function(){var t=u(this),i=t.find(".layui-colla-title"),t="none"===t.find(".layui-colla-content").css("display");i.find(".layui-colla-icon").remove(),i.append(''+(t?"":"")+""),i.off("click",C.collapse).on("click",C.collapse)})})}};return i[t]?i[t]():layui.each(i,function(t,i){i()})},new i),e=u(document);u(function(){a.render()});e.on("click",".layui-tab-title li",C.tabClick),e.on("click",C.hideTabMore),u(window).on("resize",C.tabAuto),t(c,a)});layui.define("layer",function(e){"use strict";var v=layui.$,t=layui.layer,r=layui.hint(),y=layui.device(),i={config:{},set:function(e){var t=this;return t.config=v.extend({},t.config,e),t},on:function(e,t){return layui.onevent.call(this,n,e,t)}},n="upload",o="layui-upload-file",a="layui-upload-form",F="layui-upload-iframe",b="layui-upload-choose",x=function(e){var t=this;t.config=v.extend({},t.config,i.config,e),t.render()};x.prototype.config={accept:"images",exts:"",auto:!0,bindAction:"",url:"",force:"",field:"file",acceptMime:"",method:"post",data:{},drag:!0,size:0,number:0,multiple:!1},x.prototype.render=function(e){var t=this;(e=t.config).elem=v(e.elem),e.bindAction=v(e.bindAction),t.file(),t.events()},x.prototype.file=function(){var e=this,t=e.config,i=e.elemFile=v(['"].join("")),n=t.elem.next();(n.hasClass(o)||n.hasClass(a))&&n.remove(),y.ie&&y.ie<10&&t.elem.wrap('
        '),e.isFile()?(e.elemFile=t.elem,t.field=t.elem[0].name):t.elem.after(i),y.ie&&y.ie<10&&e.initIE()},x.prototype.initIE=function(){var i,e=this.config,t=v(''),n=v(['
        ',"
        "].join(""));v("#"+F)[0]||v("body").append(t),e.elem.next().hasClass(a)||(this.elemFile.wrap(n),e.elem.next("."+a).append((i=[],layui.each(e.data,function(e,t){t="function"==typeof t?t():t,i.push('')}),i.join(""))))},x.prototype.msg=function(e){return t.msg(e,{icon:2,shift:6})},x.prototype.isFile=function(){var e=this.config.elem[0];if(e)return"input"===e.tagName.toLocaleLowerCase()&&"file"===e.type},x.prototype.preview=function(n){window.FileReader&&layui.each(this.chooseFiles,function(e,t){var i=new FileReader;i.readAsDataURL(t),i.onload=function(){n&&n(e,t,this.result)}})},x.prototype.upload=function(i,e){var n,o,t,a,l=this,r=l.config,u=l.elemFile[0],c=function(){var t=0,o=0,e=i||l.files||l.chooseFiles||u.files,a=function(){r.multiple&&t+o===l.fileLength&&"function"==typeof r.allDone&&r.allDone({total:l.fileLength,successful:t,failed:o})};layui.each(e,function(i,e){var n=new FormData,e=(n.append(r.field,e),layui.each(r.data,function(e,t){t="function"==typeof t?t():t,n.append(e,t)}),{url:r.url,type:"post",data:n,contentType:!1,processData:!1,dataType:"json",headers:r.headers||{},success:function(e){t++,f(i,e),a()},error:function(e){o++,l.msg("Request URL is abnormal: "+(e.statusText||"error")),p(i),a()}});"function"==typeof r.progress&&(e.xhr=function(){var e=v.ajaxSettings.xhr();return e.upload.addEventListener("progress",function(e){var t;e.lengthComputable&&(t=Math.floor(e.loaded/e.total*100),r.progress(t,(r.item||r.elem)[0],e,i))}),e}),v.ajax(e)})},s=function(){var n=v("#"+F);l.elemFile.parent().submit(),clearInterval(x.timer),x.timer=setInterval(function(){var e,t=n.contents().find("body");try{e=t.text()}catch(i){l.msg("Cross-domain requests are not supported"),clearInterval(x.timer),p()}e&&(clearInterval(x.timer),t.html(""),f(0,e))},30)},f=function(e,t){if(l.elemFile.next("."+b).remove(),u.value="","json"===r.force&&"object"!=typeof t)try{t=JSON.parse(t)}catch(i){return t={},l.msg("Please return JSON data format")}"function"==typeof r.done&&r.done(t,e||0,function(e){l.upload(e)})},p=function(e){r.auto&&(u.value=""),"function"==typeof r.error&&r.error(e||0,function(e){l.upload(e)})},d=r.exts,m=(o=[],layui.each(i||l.chooseFiles,function(e,t){o.push(t.name)}),o),h={preview:function(e){l.preview(e)},upload:function(e,t){var i={};i[e]=t,l.upload(i)},pushFile:function(){return l.files=l.files||{},layui.each(l.chooseFiles,function(e,t){l.files[e]=t}),l.files},resetFile:function(e,t,i){t=new File([t],i);l.files=l.files||{},l.files[e]=t}},g={file:"\u6587\u4ef6",images:"\u56fe\u7247",video:"\u89c6\u9891",audio:"\u97f3\u9891"}[r.accept]||"\u6587\u4ef6",m=0===m.length?u.value.match(/[^\/\\]+\..+/g)||[]||"":m;if(0!==m.length){switch(r.accept){case"file":layui.each(m,function(e,t){if(d&&!RegExp(".\\.("+d+")$","i").test(escape(t)))return n=!0});break;case"video":layui.each(m,function(e,t){if(!RegExp(".\\.("+(d||"avi|mp4|wma|rmvb|rm|flash|3gp|flv")+")$","i").test(escape(t)))return n=!0});break;case"audio":layui.each(m,function(e,t){if(!RegExp(".\\.("+(d||"mp3|wav|mid")+")$","i").test(escape(t)))return n=!0});break;default:layui.each(m,function(e,t){if(!RegExp(".\\.("+(d||"jpg|png|gif|bmp|jpeg")+")$","i").test(escape(t)))return n=!0})}if(n)return l.msg("\u9009\u62e9\u7684"+g+"\u4e2d\u5305\u542b\u4e0d\u652f\u6301\u7684\u683c\u5f0f"),u.value="";if("choose"!==e&&!r.auto||(r.choose&&r.choose(h),"choose"!==e)){if(l.fileLength=(t=0,g=i||l.files||l.chooseFiles||u.files,layui.each(g,function(){t++}),t),r.number&&l.fileLength>r.number)return l.msg("\u540c\u65f6\u6700\u591a\u53ea\u80fd\u4e0a\u4f20: "+r.number+" \u4e2a\u6587\u4ef6
        \u60a8\u5f53\u524d\u5df2\u7ecf\u9009\u62e9\u4e86: "+l.fileLength+" \u4e2a\u6587\u4ef6");if(01024*r.size&&(t=1<=(t=r.size/1024)?t.toFixed(2)+"MB":r.size+"KB",u.value="",a=t)}),a)return l.msg("\u6587\u4ef6\u5927\u5c0f\u4e0d\u80fd\u8d85\u8fc7 "+a);if(!r.before||!1!==r.before(h))y.ie?(9'+e+"")};o.elem.off("upload.start").on("upload.start",function(){var e=v(this),t=e.attr("lay-data");if(t)try{t=new Function("return "+t)(),n.config=v.extend({},o,t)}catch(i){r.error("Upload element property lay-data configuration item has a syntax error: "+t)}n.config.item=e,n.elemFile[0].click()}),y.ie&&y.ie<10||o.elem.off("upload.over").on("upload.over",function(){v(this).attr("lay-over","")}).off("upload.leave").on("upload.leave",function(){v(this).removeAttr("lay-over")}).off("upload.drop").on("upload.drop",function(e,t){var i=v(this),t=t.originalEvent.dataTransfer.files||[];i.removeAttr("lay-over"),a(t),o.auto?n.upload():l(t)}),n.elemFile.off("upload.change").on("upload.change",function(){var e=this.files||[];a(e),o.auto?n.upload():l(e)}),o.bindAction.off("upload.action").on("upload.action",function(){n.upload()}),o.elem.data("haveEvents")||(n.elemFile.on("change",function(){v(this).trigger("upload.change")}),o.elem.on("click",function(){n.isFile()||v(this).trigger("upload.start")}),o.drag&&o.elem.on("dragover",function(e){e.preventDefault(),v(this).trigger("upload.over")}).on("dragleave",function(e){v(this).trigger("upload.leave")}).on("drop",function(e){e.preventDefault(),v(this).trigger("upload.drop",e)}),o.bindAction.on("click",function(){v(this).trigger("upload.action")}),o.elem.data("haveEvents",!0))},i.render=function(e){e=new x(e);return function(){var t=this;return{upload:function(e){t.upload.call(t,e)},reload:function(e){t.reload.call(t,e)},config:t.config}}.call(e)},e(n,i)});layui.define(["layer","util"],function(e){"use strict";var C=layui.$,h=layui.layer,d=layui.util,l=layui.hint(),w=(layui.device(),"form"),o=".layui-form",T="layui-this",$="layui-hide",E="layui-disabled",t=function(){this.config={verify:{required:[/[\S]+/,"\u5fc5\u586b\u9879\u4e0d\u80fd\u4e3a\u7a7a"],phone:[/^1\d{10}$/,"\u8bf7\u8f93\u5165\u6b63\u786e\u7684\u624b\u673a\u53f7"],email:[/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,"\u90ae\u7bb1\u683c\u5f0f\u4e0d\u6b63\u786e"],url:[/^(#|(http(s?)):\/\/|\/\/)[^\s]+\.[^\s]+$/,"\u94fe\u63a5\u683c\u5f0f\u4e0d\u6b63\u786e"],number:function(e){if(!e||isNaN(e))return"\u53ea\u80fd\u586b\u5199\u6570\u5b57"},date:[/^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/,"\u65e5\u671f\u683c\u5f0f\u4e0d\u6b63\u786e"],identity:[/(^\d{15}$)|(^\d{17}(x|X|\d)$)/,"\u8bf7\u8f93\u5165\u6b63\u786e\u7684\u8eab\u4efd\u8bc1\u53f7"]},autocomplete:null}},i=(t.prototype.set=function(e){return C.extend(!0,this.config,e),this},t.prototype.verify=function(e){return C.extend(!0,this.config.verify,e),this},t.prototype.getFormElem=function(e){return C(o+(e?'[lay-filter="'+e+'"]':""))},t.prototype.on=function(e,t){return layui.onevent.call(this,w,e,t)},t.prototype.val=function(e,i){return this.getFormElem(e).each(function(e,t){var a=C(this);layui.each(i,function(e,t){var i,e=a.find('[name="'+e+'"]');e[0]&&("checkbox"===(i=e[0].type)?e[0].checked=t:"radio"===i?e.each(function(){this.value==t&&(this.checked=!0)}):e.val(t))})}),r.render(null,e),this.getValue(e)},t.prototype.getValue=function(e,t){t=t||this.getFormElem(e);var a={},n={},e=t.find("input,select,textarea");return layui.each(e,function(e,t){var i;C(this);t.name=(t.name||"").replace(/^\s*|\s*&/,""),t.name&&(/^.*\[\]$/.test(t.name)&&(i=t.name.match(/^(.*)\[\]$/g)[0],a[i]=0|a[i],i=t.name.replace(/^(.*)\[\]$/,"$1["+a[i]+++"]")),/^checkbox|radio$/.test(t.type)&&!t.checked||(n[i||t.name]=t.value))}),n},t.prototype.render=function(e,t){var i=this.config,a=C(o+(t?'[lay-filter="'+t+'"]':"")),n={input:function(e){e=e||a.find("input,textarea");i.autocomplete&&e.attr("autocomplete",i.autocomplete)},select:function(e){var p,c="\u8bf7\u9009\u62e9",m="layui-form-select",g="layui-select-title",k="layui-select-none",x="",e=e||a.find("select"),b=function(e,t){C(e.target).parent().hasClass(g)&&!t||(C("."+m).removeClass(m+"ed "+m+"up"),p&&x&&p.val(x)),p=null},u=function(a,e,t){var s,r,i,n,o,l,c=C(this),u=a.find("."+g),d=u.find("input"),f=a.find("dl"),h=f.children("dd"),y=f.children("dt"),v=this.selectedIndex;e||(r=c.attr("lay-search"),i=function(){var e=a.offset().top+a.outerHeight()+5-q.scrollTop(),t=f.outerHeight();v=c[0].selectedIndex,a.addClass(m+"ed"),h.removeClass($),y.removeClass($),s=null,h.eq(v).addClass(T).siblings().removeClass(T),e+t>q.height()&&t<=e&&a.addClass(m+"up"),o()},n=function(e){a.removeClass(m+"ed "+m+"up"),d.blur(),s=null,e||l(d.val(),function(e){var t=c[0].selectedIndex;e&&(x=C(c[0].options[t]).html(),0===t&&x===d.attr("placeholder")&&(x=""),d.val(x||""))})},o=function(){var e,t,i=f.children("dd."+T);i[0]&&(e=i.position().top,t=f.height(),i=i.height(),t\u65e0\u5339\u914d\u9879

        '):f.find("."+k).remove()},"keyup"),""===t&&f.find("."+k).remove(),o()}).on("blur",function(e){var t=c[0].selectedIndex;p=d,x=C(c[0].options[t]).html(),0===t&&x===d.attr("placeholder")&&(x=""),setTimeout(function(){l(d.val(),function(e){x||d.val("")},"blur")},200)}),h.on("click",function(){var e=C(this),t=e.attr("lay-value"),i=c.attr("lay-filter");return e.hasClass(E)||(e.hasClass("layui-select-tips")?d.val(""):(d.val(e.text()),e.addClass(T)),e.siblings().removeClass(T),c.val(t).removeClass("layui-form-danger"),layui.event.call(this,w,"select("+i+")",{elem:c[0],value:t,othis:a}),n(!0)),!1}),a.find("dl>dt").on("click",function(e){return!1}),C(document).off("click",b).on("click",b))};e.each(function(e,t){var i=C(this),a=i.next("."+m),n=this.disabled,l=t.value,r=C(t.options[t.selectedIndex]),t=t.options[0];if("string"==typeof i.attr("lay-ignore"))return i.show();var s,o="string"==typeof i.attr("lay-search"),t=t&&!t.value&&t.innerHTML||c,r=C(['
        ','
        ','','
        ','
        ',(t=i.find("*"),s=[],layui.each(t,function(e,t){0!==e||t.value?"optgroup"===t.tagName.toLowerCase()?s.push("
        "+t.label+"
        "):s.push('
        '+C.trim(t.innerHTML)+"
        "):s.push('
        '+C.trim(t.innerHTML||c)+"
        ")}),0===s.length&&s.push('
        \u6ca1\u6709\u9009\u9879
        '),s.join("")+"
        "),"
        "].join(""));a[0]&&a.remove(),i.after(r),u.call(this,r,n,o)})},checkbox:function(e){var o={checkbox:["layui-form-checkbox","layui-form-checked","checkbox"],_switch:["layui-form-switch","layui-form-onswitch","switch"]},e=e||a.find("input[type=checkbox]");e.each(function(e,t){var i=C(this),a=i.attr("lay-skin"),n=(i.attr("lay-text")||"").split("|"),l=this.disabled,r=o[a="switch"===a?"_"+a:a]||o.checkbox;if("string"==typeof i.attr("lay-ignore"))return i.show();var s=i.next("."+r[0]),t=C(['
        ",(l={checkbox:[t.title.replace(/\s/g,"")?""+t.title+"":"",''].join(""),_switch:""+((t.checked?n[0]:n[1])||"")+""})[a]||l.checkbox,"
        "].join(""));s[0]&&s.remove(),i.after(t),function(i,a){var n=C(this);i.on("click",function(){var e=n.attr("lay-filter"),t=(n.attr("lay-text")||"").split("|");n[0].disabled||(n[0].checked?(n[0].checked=!1,i.removeClass(a[1]).find("em").text(t[1])):(n[0].checked=!0,i.addClass(a[1]).find("em").text(t[0])),layui.event.call(n[0],w,a[2]+"("+e+")",{elem:n[0],value:n[0].value,othis:i}))})}.call(this,t,r)})},radio:function(e){var r="layui-form-radio",s=["",""],e=e||a.find("input[type=radio]");e.each(function(e,t){var i=C(this),a=i.next("."+r),n=this.disabled;if("string"==typeof i.attr("lay-ignore"))return i.show();a[0]&&a.remove();n=C(['
        ',''+s[t.checked?0:1]+"","
        "+(a=t.title||"",a="string"==typeof i.next().attr("lay-radio")?i.next().html():a)+"
        ","
        "].join(""));i.after(n),function(a){var n=C(this),l="layui-anim-scaleSpring";a.on("click",function(){var e=n[0].name,t=n.parents(o),i=n.attr("lay-filter"),e=t.find("input[name="+e.replace(/(\.|#|\[|\])/g,"\\$1")+"]");n[0].disabled||(layui.each(e,function(){var e=C(this).next("."+r);this.checked=!1,e.removeClass(r+"ed"),e.find(".layui-icon").removeClass(l).html(s[1])}),n[0].checked=!0,a.addClass(r+"ed"),a.find(".layui-icon").addClass(l).html(s[0]),layui.event.call(n[0],w,"radio("+i+")",{elem:n[0],value:n[0].value,othis:a}))})}.call(this,n)})}};return"object"===layui.type(e)?e.each(function(e,t){var i=C(t);i.closest(o).length&&("SELECT"===t.tagName?n.select(i):"INPUT"===t.tagName&&("checkbox"===(t=t.type)||"radio"===t?n[t](i):n.input(i)))}):e?n[e]?n[e]():l.error('\u4e0d\u652f\u6301\u7684 "'+e+'" \u8868\u5355\u6e32\u67d3'):layui.each(n,function(e,t){t()}),this},t.prototype.validate=function(e){var u=null,d=r.config.verify,f="layui-form-danger";return!(e=C(e))[0]||(e.attr("lay-verify")!==undefined||!1!==this.validate(e.find("*[lay-verify]")))&&(layui.each(e,function(e,r){var s=C(this),t=(s.attr("lay-verify")||"").split("|"),o=s.attr("lay-verType"),c=s.val();if(s.removeClass(f),layui.each(t,function(e,t){var i="",a=d[t];if(a){var n="function"==typeof a?i=a(c,r):!a[0].test(c),l="select"===r.tagName.toLowerCase()||/^checkbox|radio$/.test(r.type),i=i||a[1];if("required"===t&&(i=s.attr("lay-reqText")||i),n)return"tips"===o?h.tips(i,"string"!=typeof s.attr("lay-ignore")&&l?s.next():s,{tips:1}):"alert"===o?h.alert(i,{title:"\u63d0\u793a",shadeClose:!0}):/\bstring|number\b/.test(typeof i)&&h.msg(i,{icon:5,shift:6}),setTimeout(function(){(l?s.next().find("input"):r).focus()},7),s.addClass(f),u=!0}}),u)return u}),!u)},t.prototype.submit=function(e,t){var i=C(this),e="string"==typeof e?e:i.attr("lay-filter"),a=this.getFormElem?this.getFormElem(e):i.parents(o).eq(0),n=a.find("*[lay-verify]");if(!r.validate(n))return!1;n=r.getValue(null,a),a={elem:this.getFormElem?window.event&&window.event.target:this,form:(this.getFormElem?a:i.parents("form"))[0],field:n};return"function"==typeof t&&t(a),layui.event.call(this,w,"submit("+e+")",a)}),r=new t,t=C(document),q=C(window);C(function(){r.render()}),t.on("reset",o,function(){var e=C(this).attr("lay-filter");setTimeout(function(){r.render(null,e)},50)}),t.on("submit",o,i).on("click","*[lay-submit]",i),e(w,r)});layui.define(["laytpl","laypage","form","util"],function(e){"use strict";var m=layui.$,v=layui.laytpl,c=layui.laypage,g=layui.layer,y=layui.form,b=layui.util,f=layui.hint(),h=layui.device(),x={config:{checkName:"LAY_CHECKED",indexName:"LAY_TABLE_INDEX",disabledName:"LAY_DISABLED"},cache:{},index:layui.table?layui.table.index+1e4:0,set:function(e){var t=this;return t.config=m.extend({},t.config,e),t},on:function(e,t){return layui.onevent.call(this,C,e,t)}},p=function(){var a=this,e=a.config,i=e.id||e.index;return i&&(p.that[i]=a,p.config[i]=e),{config:e,reload:function(e,t){a.reload.call(a,e,t)},reloadData:function(e,t){x.reloadData(i,e,t)},setColsWidth:function(){a.setColsWidth.call(a)},resize:function(){a.resize.call(a)}}},l=function(e){var t=p.config[e];return t||f.error(e?"The table instance with ID '"+e+"' not found":"ID argument required"),t||null},k=function(e){var t=this.config||{},a=(e=e||{}).item3,i=e.content,t=(("escape"in a?a:t).escape&&(i=b.escape(i)),e.text&&a.exportTemplet||a.templet||a.toolbar);return t&&(i="function"==typeof t?t.call(a,e.tplData,e.obj):v(m(t).html()||String(i)).render(m.extend({LAY_COL:a},e.tplData))),e.text?m("
        "+i+"
        ").text():i},C="table",w="layui-hide",r="layui-hide-v",d="layui-none",s="layui-table-view",u=".layui-table-header",T=".layui-table-body",L=".layui-table-pageview",N=".layui-table-sort",D="layui-table-edit",A="layui-table-hover",E="layui-table-col-special",_="LAY_TABLE_MOVE_DICT",t=function(e){return['',"","{{# layui.each(d.data.cols, function(i1, item1){ }}","","{{# layui.each(item1, function(i2, item2){ }}",'{{# if(item2.fixed && item2.fixed !== "right"){ left = true; } }}','{{# if(item2.fixed === "right"){ right = true; } }}',(e=e||{}).fixed&&"right"!==e.fixed?'{{# if(item2.fixed && item2.fixed !== "right"){ }}':"right"===e.fixed?'{{# if(item2.fixed === "right"){ }}':"","{{# var isSort = !(item2.colGroup) && item2.sort; }}",'",e.fixed?"{{# }; }}":"","{{# }); }}","","{{# }); }}","","
        ','
        ','{{# if(item2.type === "checkbox"){ }}','',"{{# } else { }}",'{{-item2.title||""}}',"{{# if(isSort){ }}",'',"{{# } }}","{{# } }}","
        ","
        "].join("")},a=['',"","
        "].join(""),j=[,"{{# if(d.data.toolbar){ }}",'
        ','
        ','
        ',"
        ","{{# } }}",'
        ',"{{# if(d.data.loading){ }}",'
        ','',"
        ","{{# } }}","{{# var left, right; }}",'
        ',t(),"
        ",'
        ',a,"
        ","{{# if(left){ }}",'
        ','
        ',t({fixed:!0}),"
        ",'
        ',a,"
        ","
        ","{{# }; }}","{{# if(right){ }}",'
        ','
        ',t({fixed:"right"}),'
        ',"
        ",'
        ',a,"
        ","
        ","{{# }; }}","
        ","{{# if(d.data.totalRow){ }}",'
        ','','',"
        ","
        ","{{# } }}",'
        ','
        ',"
        ",""].join(""),R=m(window),S=m(document),i=function(e){this.index=++x.index,this.config=m.extend({},this.config,x.config,e),this.render()},F=(i.prototype.config={limit:10,loading:!0,escape:!0,cellMinWidth:60,editTrigger:"click",defaultToolbar:["filter","exports","print"],autoSort:!0,text:{none:"\u65e0\u6570\u636e"}},i.prototype.render=function(e){var t=this,a=t.config;if(a.elem=m(a.elem),a.where=a.where||{},a.id=a.id||a.elem.attr("id")||t.index,a.request=m.extend({pageName:"page",limitName:"limit"},a.request),a.response=m.extend({statusName:"code",statusCode:0,msgName:"msg",dataName:"data",totalRowName:"totalRow",countName:"count"},a.response),"object"==typeof a.page&&(a.limit=a.page.limit||a.limit,a.limits=a.page.limits||a.limits,t.page=a.page.curr=a.page.curr||1,delete a.page.elem,delete a.page.jump),!a.elem[0])return t;if("reloadData"===e)return t.pullData(t.page,{type:"reloadData"});a.height&&/^full-\d+$/.test(a.height)&&(t.fullHeightGap=a.height.split("-")[1],a.height=R.height()-t.fullHeightGap),t.setInit();var i,l,e=a.elem,n=e.next("."+s),o=t.elem=m("
        ");o.addClass((i=[s,s+"-"+t.index,"layui-form","layui-border-box"],a.className&&i.push(a.className),i.join(" "))).attr({"lay-filter":"LAY-TABLE-FORM-DF-"+t.index,"lay-id":a.id,style:(i=[],a.width&&i.push("width:"+a.width+"px;"),a.height&&i.push("height:"+a.height+"px;"),i.join(""))}).html(v(j).render({data:a,index:t.index})),a.index=t.index,t.key=a.id||a.index,n[0]&&n.remove(),e.after(o),t.layTool=o.find(".layui-table-tool"),t.layBox=o.find(".layui-table-box"),t.layHeader=o.find(u),t.layMain=o.find(".layui-table-main"),t.layBody=o.find(T),t.layFixed=o.find(".layui-table-fixed"),t.layFixLeft=o.find(".layui-table-fixed-l"),t.layFixRight=o.find(".layui-table-fixed-r"),t.layTotal=o.find(".layui-table-total"),t.layPage=o.find(".layui-table-page"),t.renderToolbar(),t.renderPagebar(),t.fullSize(),1
        ','
        ','
        '].join(""),a=this.layTool.find(".layui-table-tool-temp"),i=("default"===e.toolbar?a.html(t):"string"==typeof e.toolbar&&(t=m(e.toolbar).html()||"")&&a.html(v(t).render(e)),{filter:{title:"\u7b5b\u9009\u5217",layEvent:"LAYTABLE_COLS",icon:"layui-icon-cols"},exports:{title:"\u5bfc\u51fa",layEvent:"LAYTABLE_EXPORT",icon:"layui-icon-export"},print:{title:"\u6253\u5370",layEvent:"LAYTABLE_PRINT",icon:"layui-icon-print"}}),l=[];"object"==typeof e.defaultToolbar&&layui.each(e.defaultToolbar,function(e,t){t="string"==typeof t?i[t]:t;t&&l.push('
        ')}),this.layTool.find(".layui-table-tool-self").html(l.join(""))},i.prototype.renderPagebar=function(){var e,t=this.config,a=this.layPagebar=m('
        ');t.pagebar&&((e=m(t.pagebar).html()||"")&&a.append(v(e).render(t)),this.layPage.append(a))},i.prototype.setParentCol=function(e,t){var a=this.config,i=this.layHeader.find('th[data-key="'+a.index+"-"+t+'"]'),l=parseInt(i.attr("colspan"))||0;i[0]&&(t=t.split("-"),t=a.cols[t[0]][t[1]],e?l--:l++,i.attr("colspan",l),i[l<1?"addClass":"removeClass"](w),t.colspan=l,t.hide=l<1,(a=i.data("parentkey"))&&this.setParentCol(e,a))},i.prototype.setColsPatch=function(){var a=this,e=a.config;layui.each(e.cols,function(e,t){layui.each(t,function(e,t){t.hide&&a.setParentCol(t.hide,t.parentKey)})})},i.prototype.setColsWidth=function(){var t,a,i=this,o=i.config,l=0,c=0,r=0,d=0,s=i.setInit("width"),e=(i.eachCols(function(e,t){t.hide||l++}),s=s-("line"===o.skin||"nob"===o.skin?2:l+1)-i.getScrollWidth(i.layMain[0])-1,function(n){layui.each(o.cols,function(e,l){layui.each(l,function(e,t){var a=0,i=t.minWidth||o.cellMinWidth;t?t.colGroup||t.hide||(n?r&&r'+(e||"Error")+"
        ");a[0]&&(t.layNone.remove(),a.remove()),t.layFixed.addClass(w),t.layMain.find("tbody").html(""),t.layMain.append(t.layNone=e),t.layTotal.addClass(r),t.layPage.find(L).addClass(r),x.cache[t.key]=[],t.syncCheckAll()},i.prototype.page=1,i.prototype.pullData=function(t,a){var e,i=this,l=i.config,n=l.request,o=l.response,c=function(){"object"==typeof l.initSort&&i.sort(l.initSort.field,l.initSort.type)};a=a||{},"function"==typeof l.before&&l.before(l),i.startTime=(new Date).getTime(),l.url?((e={})[n.pageName]=t,e[n.limitName]=l.limit,n=m.extend(e,l.where),l.contentType&&0==l.contentType.indexOf("application/json")&&(n=JSON.stringify(n)),i.loading(),m.ajax({type:l.method||"get",url:l.url,contentType:l.contentType,data:n,dataType:l.dataType||"json",jsonpCallback:l.jsonpCallback,headers:l.headers||{},success:function(e){(e="function"==typeof l.parseData?l.parseData(e)||e:e)[o.statusName]!=o.statusCode?(i.renderForm(),i.errorView(e[o.msgName]||'\u8fd4\u56de\u7684\u6570\u636e\u4e0d\u7b26\u5408\u89c4\u8303\uff0c\u6b63\u786e\u7684\u6210\u529f\u72b6\u6001\u7801\u5e94\u4e3a\uff1a"'+o.statusName+'": '+o.statusCode)):(i.renderData({res:e,curr:t,count:e[o.countName],type:a.type}),c(),l.time=(new Date).getTime()-i.startTime+" ms"),i.setColsWidth(),"function"==typeof l.done&&l.done(e,t,e[o.countName])},error:function(e,t){i.errorView("\u8bf7\u6c42\u5f02\u5e38\uff0c\u9519\u8bef\u63d0\u793a\uff1a"+t),i.renderForm(),i.setColsWidth(),"function"==typeof l.error&&l.error(e,t)}})):"array"===layui.type(l.data)&&(e=t*l.limit-l.limit,(n={})[o.dataName]=l.data.concat().splice(e,l.limit),n[o.countName]=l.data.length,"object"==typeof l.totalRow&&(n[o.totalRowName]=m.extend({},l.totalRow)),i.renderData({res:n,curr:t,count:n[o.countName],type:a.type}),c(),i.setColsWidth(),"function"==typeof l.done&&l.done(n,t,n[o.countName]))},i.prototype.eachCols=function(e){return x.eachCols(null,e,this.config.cols),this},i.prototype.col=function(e){try{return e=e.split("-"),this.config.cols[e[1]][e[2]]}catch(t){return f.error(t),{}}},i.prototype.renderData=function(e){var u=this,y=u.config,t=e.res,l=e.curr,a=e.count,n=e.sort,i=t[y.response.dataName]||[],t=t[y.response.totalRowName],h=[],f=[],p=[],o=function(){var s;if(y.HAS_SET_COLS_PATCH||u.setColsPatch(),y.HAS_SET_COLS_PATCH=!0,!n&&u.sortKey)return u.sort(u.sortKey.field,u.sortKey.sort,!0);layui.each(i,function(o,c){var a=[],i=[],r=[],d=o+y.limit*(l-1)+1;"array"===layui.type(c)&&0===c.length||(n||(c[x.config.indexName]=o),u.eachCols(function(e,l){var e=l.field||e,t=y.index+"-"+l.key,n=c[e];n!==undefined&&null!==n||(n=""),l.colGroup||(t=['','
        "+function(){var e,t=m.extend(!0,{LAY_INDEX:d,LAY_COL:l},c),a=x.config.checkName,i=x.config.disabledName;switch(l.type){case"checkbox":return'";case"radio":return t[a]&&(s=o),'';case"numbers":return d}return l.toolbar?v(m(l.toolbar).html()||"").render(t):k.call(u,{item3:l,content:n,tplData:t})}(),"
        "].join(""),a.push(t),l.fixed&&"right"!==l.fixed&&i.push(t),"right"===l.fixed&&r.push(t))}),h.push(''+a.join("")+""),f.push(''+i.join("")+""),p.push(''+r.join("")+""))}),"fixed"===y.scrollPos&&"reloadData"===e.type||u.layBody.scrollTop(0),"reset"===y.scrollPos&&u.layBody.scrollLeft(0),u.layMain.find("."+d).remove(),u.layMain.find("tbody").html(h.join("")),u.layFixLeft.find("tbody").html(f.join("")),u.layFixRight.find("tbody").html(p.join("")),u.renderForm(),"number"==typeof s&&u.setThisRowChecked(s),u.syncCheckAll(),u.fullSize(),u.haveInit?u.scrollPatch():setTimeout(function(){u.scrollPatch()},50),u.haveInit=!0,g.close(u.tipsIndex)};return x.cache[u.key]=i,u.layTotal[0==i.length?"addClass":"removeClass"](r),u.layPage[y.page||y.pagebar?"removeClass":"addClass"](w),u.layPage.find(L)[!y.page||0==a||0===i.length&&1==l?"addClass":"removeClass"](r),0===i.length?(u.renderForm(),u.errorView(y.text.none)):(u.layFixLeft.removeClass(w),n?o():(o(),u.renderTotal(i,t),u.layTotal&&u.layTotal.removeClass(w),void(y.page&&(y.page=m.extend({elem:"layui-table-page"+y.index,count:a,limit:y.limit,limits:y.limits||[10,20,30,40,50,60,70,80,90],groups:3,layout:["prev","page","next","skip","count","limit"],prev:'',next:'',jump:function(e,t){t||(u.page=e.curr,y.limit=e.limit,u.pullData(e.curr))}},y.page),y.page.count=a,c.render(y.page)))))},i.prototype.renderTotal=function(e,o){var c,r=this,d=r.config,s={};d.totalRow&&(layui.each(e,function(e,i){"array"===layui.type(i)&&0===i.length||r.eachCols(function(e,t){var e=t.field||e,a=i[e];t.totalRow&&(s[e]=(s[e]||0)+(parseFloat(a)||0))})}),r.dataTotal={},c=[],r.eachCols(function(e,t){var a,e=t.field||e,i=o&&o[t.field],l=(a=t.totalRowText||"",n="totalRowDecimals"in t?t.totalRowDecimals:2,n=parseFloat(s[e]).toFixed(n),(l={LAY_COL:t})[e]=n,n=t.totalRow&&k.call(r,{item3:t,content:n,tplData:l})||a,i||n),n=['','
        "+("string"==typeof(a=t.totalRow||d.totalRow)?v(a).render(m.extend({TOTAL_NUMS:i||s[e],LAY_COL:t},t)):l),"
        "].join("");t.field&&(r.dataTotal[e]=l),c.push(n)}),r.layTotal.find("tbody").html(""+c.join("")+""))},i.prototype.getColElem=function(e,t){var a=this.config;return e.eq(0).find(".laytable-cell-"+a.index+"-"+t+":eq(0)")},i.prototype.renderForm=function(e){this.config;var t=this.elem.attr("lay-filter");y.render(e,t)},i.prototype.setThisRowChecked=function(e){this.config;var t="layui-table-click";this.layBody.find('tr[data-index="'+e+'"]').addClass(t).siblings("tr").removeClass(t)},i.prototype.sort=function(l,e,t,a){var i,n=this,o={},c=n.config,r=c.elem.attr("lay-filter"),d=x.cache[n.key];"string"==typeof l&&(s=l,n.layHeader.find("th").each(function(e,t){var a=m(this),i=a.data("field");if(i===l)return l=a,s=i,!1}));try{var s=s||l.data("field"),u=l.data("key");if(n.sortKey&&!t&&s===n.sortKey.field&&e===n.sortKey.sort)return;var y=n.layHeader.find("th .laytable-cell-"+u).find(N);n.layHeader.find("th").find(N).removeAttr("lay-sort"),y.attr("lay-sort",e||null),n.layFixed.find("th")}catch(h){f.error("Table modules: sort field '"+s+"' not matched")}n.sortKey={field:s,sort:e},c.autoSort&&("asc"===e?i=layui.sort(d,s):"desc"===e?i=layui.sort(d,s,!0):(i=layui.sort(d,x.config.indexName),delete n.sortKey,delete c.initSort)),o[c.response.dataName]=i||d,n.renderData({res:o,curr:n.page,count:n.count,sort:!0}),a&&(c.initSort={field:s,type:e},layui.event.call(l,C,"sort("+r+")",c.initSort))},i.prototype.loading=function(e){var t=this;t.config.loading&&(e?(t.layInit&&t.layInit.remove(),delete t.layInit,t.layBox.find(".layui-table-init").remove()):(t.layInit=m(['
        ','',"
        "].join("")),t.layBox.append(t.layInit)))},i.prototype.setCheckData=function(e,t){var a=this.config,i=x.cache[this.key];i[e]&&"array"!==layui.type(i[e])&&(i[e][a.checkName]=t)},i.prototype.syncCheckAll=function(){var e=this,i=e.config,t=e.layHeader.find('input[name="layTableCheckbox"]'),a=function(a){return e.eachCols(function(e,t){"checkbox"===t.type&&(t[i.checkName]=a)}),a};t[0]&&(x.checkStatus(e.key).isAll?(t[0].checked||(t.prop("checked",!0),e.renderForm("checkbox")),a(!0)):(t[0].checked&&(t.prop("checked",!1),e.renderForm("checkbox")),a(!1)))},i.prototype.getCssRule=function(a,i){var e=this.elem.find("style")[0],e=e.sheet||e.styleSheet||{},e=e.cssRules||e.rules;layui.each(e,function(e,t){if(t.selectorText===".laytable-cell-"+a)return i(t),!0})},i.prototype.fullSize=function(){var e=this,t=e.config,a=t.height;e.fullHeightGap&&(a=R.height()-e.fullHeightGap,e.elem.css("height",a=a<135?135:a)),a&&(a=parseFloat(a)-(e.layHeader.outerHeight()||38),t.toolbar&&(a-=e.layTool.outerHeight()||50),t.totalRow&&(a-=e.layTotal.outerHeight()||40),(t.page||t.pagebar)&&(a-=e.layPage.outerHeight()||43),e.layMain.outerHeight(a))},i.prototype.getScrollWidth=function(e){var t=0;return e?t=e.offsetWidth-e.clientWidth:((e=document.createElement("div")).style.width="100px",e.style.height="100px",e.style.overflowY="scroll",document.body.appendChild(e),t=e.offsetWidth-e.clientWidth,document.body.removeChild(e)),t},i.prototype.scrollPatch=function(){var e=this,t=e.layMain.children("table"),a=e.layMain.width()-e.layMain.prop("clientWidth"),i=e.layMain.height()-e.layMain.prop("clientHeight"),l=(e.getScrollWidth(e.layMain[0]),t.outerWidth()-e.layMain.width()),n=function(e){var t;a&&i?(e=e.eq(0)).find(".layui-table-patch")[0]||((t=m('
        ')).find("div").css({width:a}),e.find("tr").append(t)):e.find(".layui-table-patch").remove()};n(e.layHeader),n(e.layTotal);n=e.layMain.height()-i;e.layFixed.find(T).css("height",t.height()>=n?n:"auto"),e.layFixRight[0');a.html(t),r.height&&a.css("max-height",r.height-(s.layTool.outerHeight()||50)),i.find(".layui-table-tool-panel")[0]||i.append(a),s.renderForm(),a.on("click",function(e){layui.stope(e)}),e.done&&e.done(a,t)};switch(layui.stope(e),S.trigger("table.tool.panel.remove"),g.close(s.tipsIndex),t){case"LAYTABLE_COLS":l({list:(a=[],s.eachCols(function(e,t){t.field&&"normal"==t.type&&a.push('
      • ')}),a.join("")),done:function(){y.on("checkbox(LAY_TABLE_TOOL_COLS)",function(e){var e=m(e.elem),i=this.checked,l=e.data("key"),n=e.data("parentkey");layui.each(r.cols,function(a,e){layui.each(e,function(e,t){a+"-"+e===l&&(e=t.hide,t.hide=!i,s.elem.find('*[data-key="'+r.index+"-"+l+'"]')[i?"removeClass":"addClass"](w),e!=t.hide&&s.setParentCol(!i,n),s.resize())})})})}});break;case"LAYTABLE_EXPORT":h.ie?g.tips("\u5bfc\u51fa\u529f\u80fd\u4e0d\u652f\u6301 IE\uff0c\u8bf7\u7528 Chrome \u7b49\u9ad8\u7ea7\u6d4f\u89c8\u5668\u5bfc\u51fa",this,{tips:3}):l({list:['
      • \u5bfc\u51fa csv \u683c\u5f0f\u6587\u4ef6
      • ','
      • \u5bfc\u51fa xls \u683c\u5f0f\u6587\u4ef6
      • '].join(""),done:function(e,t){t.on("click",function(){var e=m(this).data("type");x.exportFile.call(s,r.id,null,e)})}});break;case"LAYTABLE_PRINT":var n=window.open("about:blank","_blank"),o=[""].join(""),c=m(s.layHeader.html());c.append(s.layMain.find("table").html()),c.append(s.layTotal.find("table").html()),c.find("th.layui-table-patch").remove(),c.find("thead>tr>th."+E).filter(function(e,t){return!m(t).children(".laytable-cell-group").length}).remove(),c.find("tbody>tr>td."+E).remove(),n.document.write(o+c.prop("outerHTML")),n.document.close(),n.print(),n.close()}layui.event.call(this,C,"toolbar("+d+")",m.extend({event:t,config:r},{}))}),s.layPagebar.on("click","*[lay-event]",function(e){var t=m(this).attr("lay-event");layui.event.call(this,C,"pagebar("+d+")",m.extend({event:t,config:r},{}))}),e.on("mousemove",function(e){var t=m(this),a=t.offset().left,e=e.clientX-a;t.data("unresize")||p.eventMoveElem||(l.allowResize=t.width()-e<=10,i.css("cursor",l.allowResize?"col-resize":""))}).on("mouseleave",function(){m(this);p.eventMoveElem||i.css("cursor","")}).on("mousedown",function(e){var t,a=m(this);l.allowResize&&(t=a.data("key"),e.preventDefault(),l.offset=[e.clientX,e.clientY],s.getCssRule(t,function(e){var t=e.style.width||a.outerWidth();l.rule=e,l.ruleWidth=parseFloat(t),l.minWidth=a.data("minwidth")||r.cellMinWidth}),a.data(_,l),p.eventMoveElem=a)}),p.docEvent||S.on("mousemove",function(e){var t;p.eventMoveElem&&(t=p.eventMoveElem.data(_)||{},p.eventMoveElem.data("resizing",1),e.preventDefault(),t.rule&&((e=t.ruleWidth+e.clientX-t.offset[0])':''))[0].value=n.data("content")||a[t]||i.text(),n.find("."+D)[0]||n.append(l),l.focus(),layui.stope(e)))}).on("mouseenter","td",function(){a.call(this)}).on("mouseleave","td",function(){a.call(this,"hide")}),"layui-table-grid-down"),a=function(e){var t=m(this),a=t.children(u);t.data("off")||(e?t.find(".layui-table-grid-down").remove():!(a.prop("scrollWidth")>a.outerWidth()||0'))},c=(s.layBody.on("click","."+o,function(e){var t=m(this).parent().children(u);s.tipsIndex=g.tips(['
        ',t.html(),"
        ",''].join(""),t[0],{tips:[3,""],time:-1,anim:-1,maxWidth:h.ios||h.android?300:s.elem.width()/2,isOutAnim:!1,skin:"layui-table-tips",success:function(e,t){e.find(".layui-table-tips-c").on("click",function(){g.close(t)})}}),layui.stope(e)}),function(e){var t=m(this),a=t.parents("tr").eq(0).data("index");layui.event.call(this,C,(e||"tool")+"("+d+")",n.call(this,{event:t.attr("lay-event")})),s.setThisRowChecked(a)});s.layBody.on("click","*[lay-event]",function(e){c.call(this),layui.stope(e)}).on("dblclick","*[lay-event]",function(e){c.call(this,"toolDouble"),layui.stope(e)}),s.layMain.on("scroll",function(){var e=m(this),t=e.scrollLeft(),e=e.scrollTop();s.layHeader.scrollLeft(t),s.layTotal.scrollLeft(t),s.layFixed.find(T).scrollTop(e),g.close(s.tipsIndex)}),R.on("resize",function(){s.resize()})},S.on("click",function(){S.trigger("table.remove.tool.panel")}),S.on("table.remove.tool.panel",function(){m(".layui-table-tool-panel").remove()}),x.init=function(a,i){i=i||{};var e=m(a?'table[lay-filter="'+a+'"]':".layui-table[lay-data]"),c="Table element property lay-data configuration item has a syntax error: ";return e.each(function(){var e=m(this),t=e.attr("lay-data");try{t=new Function("return "+t)()}catch(l){f.error(c+t,"error")}var n=[],o=m.extend({elem:this,cols:[],data:[],skin:e.attr("lay-skin"),size:e.attr("lay-size"),even:"string"==typeof e.attr("lay-even")},x.config,i,t);a&&e.hide(),e.find("thead>tr").each(function(i){o.cols[i]=[],m(this).children().each(function(e){var t=m(this),a=t.attr("lay-data");try{a=new Function("return "+a)()}catch(l){return f.error(c+a)}t=m.extend({title:t.text(),colspan:t.attr("colspan")||1,rowspan:t.attr("rowspan")||1},a);t.colspan<2&&n.push(t),o.cols[i].push(t)})}),e.find("tbody>tr").each(function(e){var a=m(this),l={};a.children("td").each(function(e,t){var a=m(this),i=a.data("field");if(i)return l[i]=a.html()}),layui.each(n,function(e,t){e=a.children("td").eq(e);l[t.field]=e.html()}),o.data[e]=l}),x.render(o)}),this},p.that={},p.config={},function(a,i,e,l){var n,o;l.colGroup&&(n=0,a++,l.CHILD_COLS=[],o=e+(parseInt(l.rowspan)||1),layui.each(i[o],function(e,t){t.parentKey?t.parentKey===l.key&&(t.PARENT_COL_INDEX=a,l.CHILD_COLS.push(t),F(a,i,o,t)):t.PARENT_COL_INDEX||1<=n&&n==(l.colspan||1)||(t.PARENT_COL_INDEX=a,l.CHILD_COLS.push(t),n+=t.hide?0:parseInt(1td'),a!==undefined&&null!==a||(a=""),0==l&&c.push(t.title||""),o.push('"'+k.call(d,{item3:t,content:a,tplData:n,text:"text",obj:d.commonMember.call(i.eq(0),{td:function(e){return i.filter('[data-field="'+e+'"]')}})})+'"')))}),i.push(o.join(","))}),d&&layui.each(d.dataTotal,function(e,t){r[e]||l.push(t)}),c.join(",")+"\r\n"+i.join("\r\n")+"\r\n"+l.join(","))),u.download=(a.title||o.title||"table_"+(o.index||""))+"."+n,document.body.appendChild(u),u.click(),document.body.removeChild(u)},x.resize=function(e){e?l(e)&&p.that[e].resize():layui.each(p.that,function(){this.resize()})},x.reload=function(e,t,a,i){if(l(e))return e=p.that[e],e.reload(t,a,i),p.call(e)},x.reloadData=function(){var a=m.extend([],arguments),i=(a[3]="reloadData",new RegExp("^("+["data","url","method","contentType","dataType","jsonpCallback","headers","where","page","limit","request","response","parseData","scrollPos"].join("|")+")$"));return layui.each(a[1],function(e,t){i.test(e)||delete a[1][e]}),x.reload.apply(null,a)},x.render=function(e){e=new i(e);return p.call(e)},x.clearCacheKey=function(e){return delete(e=m.extend({},e))[x.config.checkName],delete e[x.config.indexName],delete e[x.config.disabledName],e},m(function(){x.init()}),e(C,x)});layui.define("form",function(e){"use strict";var u=layui.$,i=layui.form,p=layui.layer,n="tree",a={config:{},index:layui[n]?layui[n].index+1e4:0,set:function(e){var i=this;return i.config=u.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,n,e,i)}},t=function(){var i=this,e=i.config,n=e.id||i.index;return t.that[n]=i,{config:t.config[n]=e,reload:function(e){i.reload.call(i,e)},getChecked:function(){return i.getChecked.call(i)},setChecked:function(e){return i.setChecked.call(i,e)}}},y="layui-hide",d="layui-disabled",f="layui-tree-set",C="layui-tree-iconClick",k="layui-icon-addition",v="layui-icon-subtraction",m="layui-tree-entry",x="layui-tree-main",b="layui-tree-txt",g="layui-tree-pack",w="layui-tree-spread",N="layui-tree-setLineShort",T="layui-tree-showLine",L="layui-tree-lineExtend",l=function(e){var i=this;i.index=++a.index,i.config=u.extend({},i.config,a.config,e),i.render()};l.prototype.config={data:[],showCheckbox:!1,showLine:!0,accordion:!1,onlyIconControl:!1,isJump:!1,edit:!1,text:{defaultNodeName:"\u672a\u547d\u540d",none:"\u65e0\u6570\u636e"}},l.prototype.reload=function(e){var n=this;layui.each(e,function(e,i){"array"===layui.type(i)&&delete n.config[e]}),n.config=u.extend(!0,{},n.config,e),n.render()},l.prototype.render=function(){var e=this,i=e.config,n=(e.checkids=[],u('
        ')),a=(e.tree(n),i.elem=u(i.elem));if(a[0]){if(e.key=i.id||e.index,e.elem=n,e.elemNone=u('
        '+i.text.none+"
        "),a.html(e.elem),0==e.elem.find(".layui-tree-set").length)return e.elem.append(e.elemNone);i.showCheckbox&&e.renderForm("checkbox"),e.elem.find(".layui-tree-set").each(function(){var e=u(this);e.parent(".layui-tree-pack")[0]||e.addClass("layui-tree-setHide"),!e.next()[0]&&e.parents(".layui-tree-pack").eq(1).hasClass("layui-tree-lineExtend")&&e.addClass(N),e.next()[0]||e.parents(".layui-tree-set").eq(0).next()[0]||e.addClass(N)}),e.events()}},l.prototype.renderForm=function(e){i.render(e,"LAY-tree-"+this.index)},l.prototype.tree=function(l,e){var r=this,c=r.config,e=e||c.data;layui.each(e,function(e,i){var n=i.children&&0"),t=u(['
        ','
        ','
        ',c.showLine?n?'':'':'',c.showCheckbox?'':"",c.isJump&&i.href?''+(i.title||i.label||c.text.defaultNodeName)+"":''+(i.title||i.label||c.text.defaultNodeName)+"","
        ",function(){if(!c.edit)return"";var n={add:'',update:'',del:''},a=['
        '];return!0===c.edit&&(c.edit=["update","del"]),"object"==typeof c.edit?(layui.each(c.edit,function(e,i){a.push(n[i]||"")}),a.join("")+"
        "):void 0}(),"
        "].join(""));n&&(t.append(a),r.tree(a,i.children)),l.append(t),t.prev("."+f)[0]&&t.prev().children(".layui-tree-pack").addClass("layui-tree-showLine"),n||t.parent(".layui-tree-pack").addClass("layui-tree-lineExtend"),r.spread(t,i),c.showCheckbox&&(i.checked&&r.checkids.push(i.id),r.checkClick(t,i)),c.edit&&r.operate(t,i)})},l.prototype.spread=function(a,e){var t=this.config,i=a.children("."+m),n=i.children("."+x),l=i.find("."+C),i=i.find("."+b),r=t.onlyIconControl?l:n,c="";r.on("click",function(e){var i=a.children("."+g),n=(r.children(".layui-icon")[0]?r:r.find(".layui-tree-icon")).children(".layui-icon");i[0]?a.hasClass(w)?(a.removeClass(w),i.slideUp(200),n.removeClass(v).addClass(k)):(a.addClass(w),i.slideDown(200),n.addClass(v).removeClass(k),t.accordion&&((i=a.siblings("."+f)).removeClass(w),i.children("."+g).slideUp(200),i.find(".layui-tree-icon").children(".layui-icon").removeClass(v).addClass(k))):c="normal"}),i.on("click",function(){u(this).hasClass(d)||(c=a.hasClass(w)?t.onlyIconControl?"open":"close":t.onlyIconControl?"close":"open",t.click&&t.click({elem:a,state:c,data:e}))})},l.prototype.setCheckbox=function(e,i,n){this.config;var t,l=n.prop("checked");n.prop("disabled")||("object"!=typeof i.children&&!e.find("."+g)[0]||e.find("."+g).find('input[same="layuiTreeCheck"]').each(function(){this.disabled||(this.checked=l)}),(t=function(e){var i,n,a;e.parents("."+f)[0]&&(n=(e=e.parent("."+g)).parent(),a=e.prev().find('input[same="layuiTreeCheck"]'),l?a.prop("checked",l):(e.find('input[same="layuiTreeCheck"]').each(function(){this.checked&&(i=!0)}),i||a.prop("checked",!1)),t(n))})(e),this.renderForm("checkbox"))},l.prototype.checkClick=function(n,a){var t=this,l=t.config;n.children("."+m).children("."+x).on("click",'input[same="layuiTreeCheck"]+',function(e){layui.stope(e);var e=u(this).prev(),i=e.prop("checked");e.prop("disabled")||(t.setCheckbox(n,a,e),l.oncheck&&l.oncheck({elem:n,checked:i,data:a}))})},l.prototype.operate=function(c,d){var s=this,o=s.config,e=c.children("."+m),h=e.children("."+x);e.children(".layui-tree-btnGroup").on("click",".layui-icon",function(e){layui.stope(e);var i,e=u(this).data("type"),a=c.children("."+g),t={data:d,type:e,elem:c};if("add"==e){a[0]||(o.showLine?(h.find("."+C).addClass("layui-tree-icon"),h.find("."+C).children(".layui-icon").addClass(k).removeClass("layui-icon-file")):h.find(".layui-tree-iconArrow").removeClass(y),c.append('
        '));var n,l=o.operate&&o.operate(t),r={};if(r.title=o.text.defaultNodeName,r.id=l,s.tree(c.children("."+g),[r]),o.showLine&&(a[0]?(a.hasClass(L)||a.addClass(L),c.find("."+g).each(function(){u(this).children("."+f).last().addClass(N)}),(a.children("."+f).last().prev().hasClass(N)?a.children("."+f).last().prev():a.children("."+f).last()).removeClass(N),!c.parent("."+g)[0]&&c.next()[0]&&a.children("."+f).last().removeClass(N)):(l=c.siblings("."+f),n=1,r=c.parent("."+g),layui.each(l,function(e,i){u(i).children("."+g)[0]||(n=0)}),1==n?(l.children("."+g).addClass(T),l.children("."+g).children("."+f).removeClass(N),c.children("."+g).addClass(T),r.removeClass(L),r.children("."+f).last().children("."+g).children("."+f).last().addClass(N)):c.children("."+g).children("."+f).addClass(N))),!o.showCheckbox)return;h.find('input[same="layuiTreeCheck"]')[0].checked&&(c.children("."+g).children("."+f).last().find('input[same="layuiTreeCheck"]')[0].checked=!0),s.renderForm("checkbox")}else"update"==e?(l=h.children("."+b).html(),h.children("."+b).html(""),h.append(''),h.children(".layui-tree-editInput").val(l).focus(),i=function(e){var i=(i=e.val().trim())||o.text.defaultNodeName;e.remove(),h.children("."+b).html(i),t.data.title=i,o.operate&&o.operate(t)},h.children(".layui-tree-editInput").blur(function(){i(u(this))}),h.children(".layui-tree-editInput").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),i(u(this)))})):p.confirm('\u786e\u8ba4\u5220\u9664\u8be5\u8282\u70b9 "'+(d.title||"")+'" \u5417\uff1f',function(e){if(o.operate&&o.operate(t),t.status="remove",p.close(e),!c.prev("."+f)[0]&&!c.next("."+f)[0]&&!c.parent("."+g)[0])return c.remove(),void s.elem.append(s.elemNone);var l,n,i;c.siblings("."+f).children("."+m)[0]?(o.showCheckbox&&(l=function(e){var i,n,a,t;e.parents("."+f)[0]&&(i=e.siblings("."+f).children("."+m),n=(e=e.parent("."+g).prev()).find('input[same="layuiTreeCheck"]')[0],a=1,(t=0)==n.checked&&(i.each(function(e,i){i=u(i).find('input[same="layuiTreeCheck"]')[0];0!=i.checked||i.disabled||(a=0),i.disabled||(t=1)}),1==a&&1==t&&(n.checked=!0,s.renderForm("checkbox"),l(e.parent("."+f)))))})(c),o.showLine&&(e=c.siblings("."+f),n=1,i=c.parent("."+g),layui.each(e,function(e,i){u(i).children("."+g)[0]||(n=0)}),1==n?(a[0]||(i.removeClass(L),e.children("."+g).addClass(T),e.children("."+g).children("."+f).removeClass(N)),(c.next()[0]?i.children("."+f).last():c.prev()).children("."+g).children("."+f).last().addClass(N),c.next()[0]||c.parents("."+f)[1]||c.parents("."+f).eq(0).next()[0]||c.prev("."+f).addClass(N)):!c.next()[0]&&c.hasClass(N)&&c.prev().addClass(N))):(e=c.parent("."+g).prev(),o.showLine?(e.find("."+C).removeClass("layui-tree-icon"),e.find("."+C).children(".layui-icon").removeClass(v).addClass("layui-icon-file"),(i=e.parents("."+g).eq(0)).addClass(L),i.children("."+f).each(function(){u(this).children("."+g).children("."+f).last().addClass(N)})):e.find(".layui-tree-iconArrow").addClass(y),c.parents("."+f).eq(0).removeClass(w),c.parent("."+g).remove()),c.remove()})})},l.prototype.events=function(){var i=this,t=i.config;i.elem.find(".layui-tree-checkedFirst");i.setChecked(i.checkids),i.elem.find(".layui-tree-search").on("keyup",function(){var e=u(this),n=e.val(),e=e.nextAll(),a=[];e.find("."+b).each(function(){var i,e=u(this).parents("."+m);-1!=u(this).html().indexOf(n)&&(a.push(u(this).parent()),(i=function(e){e.addClass("layui-tree-searchShow"),e.parent("."+g)[0]&&i(e.parent("."+g).parent("."+f))})(e.parent("."+f)))}),e.find("."+m).each(function(){var e=u(this).parent("."+f);e.hasClass("layui-tree-searchShow")||e.addClass(y)}),0==e.find(".layui-tree-searchShow").length&&i.elem.append(i.elemNone),t.onsearch&&t.onsearch({elem:a})}),i.elem.find(".layui-tree-search").on("keydown",function(){u(this).nextAll().find("."+m).each(function(){u(this).parent("."+f).removeClass("layui-tree-searchShow "+y)}),u(".layui-tree-emptyText")[0]&&u(".layui-tree-emptyText").remove()})},l.prototype.getChecked=function(){var e=this.config,i=[],n=[],t=(this.elem.find(".layui-form-checked").each(function(){i.push(u(this).prev()[0].value)}),function(e,a){layui.each(e,function(e,n){layui.each(i,function(e,i){if(n.id==i)return delete(i=u.extend({},n)).children,a.push(i),n.children&&(i.children=[],t(n.children,i.children)),!0})})});return t(u.extend({},e.data),n),n},l.prototype.setChecked=function(l){this.config;this.elem.find("."+f).each(function(e,i){var n=u(this).data("id"),a=u(i).children("."+m).find('input[same="layuiTreeCheck"]'),t=a.next();if("number"==typeof l){if(n==l)return a[0].checked||t.click(),!1}else"object"==typeof l&&layui.each(l,function(e,i){if(i==n&&!a[0].checked)return t.click(),!0})})},t.that={},t.config={},a.reload=function(e,i){e=t.that[e];return e.reload(i),t.call(e)},a.getChecked=function(e){return t.that[e].getChecked()},a.setChecked=function(e,i){return t.that[e].setChecked(i)},a.render=function(e){e=new l(e);return t.call(e)},e(n,a)});layui.define(["laytpl","form"],function(e){"use strict";var s=layui.$,n=layui.laytpl,t=layui.form,a="transfer",i={config:{},index:layui[a]?layui[a].index+1e4:0,set:function(e){var t=this;return t.config=s.extend({},t.config,e),t},on:function(e,t){return layui.onevent.call(this,a,e,t)}},l=function(){var t=this,e=t.config,a=e.id||t.index;return l.that[a]=t,{config:l.config[a]=e,reload:function(e){t.reload.call(t,e)},getData:function(){return t.getData.call(t)}}},d="layui-hide",h="layui-btn-disabled",r="layui-none",c="layui-transfer-box",u="layui-transfer-header",o="layui-transfer-search",f="layui-transfer-data",y=function(e){return['
        ','
        ','","
        ","{{# if(d.data.showSearch){ }}",'","{{# } }}",'
          ',"
          "].join("")},p=['
          ',y({index:0,checkAllName:"layTransferLeftCheckAll"}),'
          ','",'","
          ",y({index:1,checkAllName:"layTransferRightCheckAll"}),"
          "].join(""),v=function(e){var t=this;t.index=++i.index,t.config=s.extend({},t.config,i.config,e),t.render()};v.prototype.config={title:["\u5217\u8868\u4e00","\u5217\u8868\u4e8c"],width:200,height:360,data:[],value:[],showSearch:!1,id:"",text:{none:"\u65e0\u6570\u636e",searchNone:"\u65e0\u5339\u914d\u6570\u636e"}},v.prototype.reload=function(e){var t=this;t.config=s.extend({},t.config,e),t.render()},v.prototype.render=function(){var e=this,t=e.config,a=e.elem=s(n(p).render({data:t,index:e.index})),i=t.elem=s(t.elem);i[0]&&(t.data=t.data||[],t.value=t.value||[],e.key=t.id||e.index,i.html(e.elem),e.layBox=e.elem.find("."+c),e.layHeader=e.elem.find("."+u),e.laySearch=e.elem.find("."+o),e.layData=a.find("."+f),e.layBtn=a.find(".layui-transfer-active .layui-btn"),e.layBox.css({width:t.width,height:t.height}),e.layData.css({height:(i=t.height-e.layHeader.outerHeight(),t.showSearch&&(i-=e.laySearch.outerHeight()),i-2)}),e.renderData(),e.events())},v.prototype.renderData=function(){var e=this,i=(e.config,[{checkName:"layTransferLeftCheck",views:[]},{checkName:"layTransferRightCheck",views:[]}]);e.parseData(function(e){var t=e.selected?1:0,a=["
        • ",'',"
        • "].join("");i[t].views.push(a),delete e.selected}),e.layData.eq(0).html(i[0].views.join("")),e.layData.eq(1).html(i[1].views.join("")),e.renderCheckBtn()},v.prototype.renderForm=function(e){t.render(e,"LAY-transfer-"+this.index)},v.prototype.renderCheckBtn=function(r){var c=this,o=c.config;r=r||{},c.layBox.each(function(e){var t=s(this),a=t.find("."+f),t=t.find("."+u).find('input[type="checkbox"]'),i=a.find('input[type="checkbox"]'),n=0,l=!1;i.each(function(){var e=s(this).data("hide");(this.checked||this.disabled||e)&&n++,this.checked&&!e&&(l=!0)}),t.prop("checked",l&&n===i.length),c.layBtn.eq(e)[l?"removeClass":"addClass"](h),r.stopNone||(i=a.children("li:not(."+d+")").length,c.noneView(a,i?"":o.text.none))}),c.renderForm("checkbox")},v.prototype.noneView=function(e,t){var a=s('

          '+(t||"")+"

          ");e.find("."+r)[0]&&e.find("."+r).remove(),t.replace(/\s/g,"")&&e.append(a)},v.prototype.setValue=function(){var e=this.config,t=[];return this.layBox.eq(1).find("."+f+' input[type="checkbox"]').each(function(){s(this).data("hide")||t.push(this.value)}),e.value=t,this},v.prototype.parseData=function(t){var i=this.config,n=[];return layui.each(i.data,function(e,a){a=("function"==typeof i.parseData?i.parseData(a):a)||a,n.push(a=s.extend({},a)),layui.each(i.value,function(e,t){t==a.value&&(a.selected=!0)}),t&&t(a)}),i.data=n,this},v.prototype.getData=function(e){var t=this.config,i=[];return this.setValue(),layui.each(e||t.value,function(e,a){layui.each(t.data,function(e,t){delete t.selected,a==t.value&&i.push(t)})}),i},v.prototype.transfer=function(e,t){var a,i=this,n=i.config,l=i.layBox.eq(e),r=[],t=(t?((a=(t=t).find('input[type="checkbox"]'))[0].checked=!1,l.siblings("."+c).find("."+f).append(t.clone()),t.remove(),r.push(a[0].value),i.setValue()):l.each(function(e){s(this).find("."+f).children("li").each(function(){var e=s(this),t=e.find('input[type="checkbox"]'),a=t.data("hide");t[0].checked&&!a&&(t[0].checked=!1,l.siblings("."+c).find("."+f).append(e.clone()),e.remove(),r.push(t[0].value)),i.setValue()})}),i.renderCheckBtn(),l.siblings("."+c).find("."+o+" input"));""!==t.val()&&t.trigger("keyup"),n.onchange&&n.onchange(i.getData(r),e)},v.prototype.events=function(){var n=this,l=n.config;n.elem.on("click",'input[lay-filter="layTransferCheckbox"]+',function(){var e=s(this).prev(),t=e[0].checked,a=e.parents("."+c).eq(0).find("."+f);e[0].disabled||("all"===e.attr("lay-type")&&a.find('input[type="checkbox"]').each(function(){this.disabled||(this.checked=t)}),setTimeout(function(){n.renderCheckBtn({stopNone:!0})},0))}),n.elem.on("dblclick","."+f+">li",function(e){var t=s(this),a=t.children('input[type="checkbox"]'),i=t.parent().parent();a[0].disabled||n.transfer(i.data("index"),t)}),n.layBtn.on("click",function(){var e=s(this),t=e.data("index");e.hasClass(h)||n.transfer(t)}),n.laySearch.find("input").on("keyup",function(){var i=this.value,e=s(this).parents("."+o).eq(0).siblings("."+f),t=e.children("li"),t=(t.each(function(){var e=s(this),t=e.find('input[type="checkbox"]'),a=t[0].title,a=("cs"!==l.showSearch&&(a=a.toLowerCase(),i=i.toLowerCase()),-1!==a.indexOf(i));e[a?"removeClass":"addClass"](d),t.data("hide",!a)}),n.renderCheckBtn(),t.length===e.children("li."+d).length);n.noneView(e,t?l.text.searchNone:"")})},l.that={},l.config={},i.reload=function(e,t){e=l.that[e];return e.reload(t),l.call(e)},i.getData=function(e){return l.that[e].getData()},i.render=function(e){e=new v(e);return l.call(e)},e(a,i)});layui.define("jquery",function(e){"use strict";var a=layui.$,n=(layui.hint(),layui.device(),{config:{},set:function(e){var i=this;return i.config=a.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,d,e,i)}}),d="carousel",r="layui-this",s="layui-carousel-left",u="layui-carousel-right",c="layui-carousel-prev",m="layui-carousel-next",t="layui-carousel-arrow",l="layui-carousel-ind",i=function(e){var i=this;i.config=a.extend({},i.config,n.config,e),i.render()};i.prototype.config={width:"600px",height:"280px",full:!1,arrow:"hover",indicator:"inside",autoplay:!0,interval:3e3,anim:"",trigger:"click",index:0},i.prototype.render=function(){var e=this,i=e.config;i.elem=a(i.elem),i.elem[0]&&(e.elemItem=i.elem.find(">*[carousel-item]>*"),i.index<0&&(i.index=0),i.index>=e.elemItem.length&&(i.index=e.elemItem.length-1),i.interval<800&&(i.interval=800),i.full?i.elem.css({position:"fixed",width:"100%",height:"100%",zIndex:9999}):i.elem.css({width:i.width,height:i.height}),i.elem.attr("lay-anim",i.anim),e.elemItem.eq(i.index).addClass(r),e.elemItem.length<=1||(e.indicator(),e.arrow(),e.autoplay(),e.events()))},i.prototype.reload=function(e){var i=this;clearInterval(i.timer),i.config=a.extend({},i.config,e),i.render()},i.prototype.prevIndex=function(){var e=this.config.index-1;return e=e<0?this.elemItem.length-1:e},i.prototype.nextIndex=function(){var e=this.config.index+1;return e=e>=this.elemItem.length?0:e},i.prototype.addIndex=function(e){var i=this.config;i.index=i.index+(e=e||1),i.index>=this.elemItem.length&&(i.index=0)},i.prototype.subIndex=function(e){var i=this.config;i.index=i.index-(e=e||1),i.index<0&&(i.index=this.elemItem.length-1)},i.prototype.autoplay=function(){var e=this,i=e.config;i.autoplay&&(clearInterval(e.timer),e.timer=setInterval(function(){e.slide()},i.interval))},i.prototype.arrow=function(){var i=this,e=i.config,n=a(['",'"].join(""));e.elem.attr("lay-arrow",e.arrow),e.elem.find("."+t)[0]&&e.elem.find("."+t).remove(),e.elem.append(n),n.on("click",function(){var e=a(this).attr("lay-type");i.slide(e)})},i.prototype.indicator=function(){var i,n=this,t=n.config,e=n.elemInd=a(['
            ',(i=[],layui.each(n.elemItem,function(e){i.push("")}),i.join("")),"
          "].join(""));t.elem.attr("lay-indicator",t.indicator),t.elem.find("."+l)[0]&&t.elem.find("."+l).remove(),t.elem.append(e),"updown"===t.anim&&e.css("margin-top",-e.height()/2),e.find("li").on("hover"===t.trigger?"mouseover":t.trigger,function(){var e=a(this).index();e>t.index?n.slide("add",e-t.index):ea.length&&(a.value=a.length),parseInt(a.value)===a.value||a.half||(a.value=Math.ceil(a.value)-a.value<.5?Math.ceil(a.value):Math.floor(a.value)),'
            "),n=1;n<=a.length;n++){var t='
          • ";a.half&&parseInt(a.value)!==a.value&&n==Math.ceil(a.value)?i=i+'
          • ":i+=t}i+="
          "+(a.text?''+a.value+"\u661f":"")+"";var o=a.elem,s=o.next(".layui-rate");s[0]&&s.remove(),e.elemTemp=u(i),a.span=e.elemTemp.next("span"),a.setText&&a.setText(a.value),o.html(e.elemTemp),o.addClass("layui-inline"),a.readonly||e.action()},a.prototype.setvalue=function(e){this.config.value=e,this.render()},a.prototype.action=function(){var i=this.config,n=this.elemTemp,t=n.find("i").width();n.children("li").each(function(e){var a=e+1,l=u(this);l.on("click",function(e){i.value=a,i.half&&e.pageX-u(this).offset().left<=t/2&&(i.value=i.value-.5),i.text&&n.next("span").text(i.value+"\u661f"),i.choose&&i.choose(i.value),i.setText&&i.setText(i.value)}),l.on("mousemove",function(e){n.find("i").each(function(){u(this).addClass(c).removeClass(s)}),n.find("i:lt("+a+")").each(function(){u(this).addClass(r).removeClass(f)}),i.half&&e.pageX-u(this).offset().left<=t/2&&l.children("i").addClass(o).removeClass(r)}),l.on("mouseleave",function(){n.find("i").each(function(){u(this).addClass(c).removeClass(s)}),n.find("i:lt("+Math.floor(i.value)+")").each(function(){u(this).addClass(r).removeClass(f)}),i.half&&parseInt(i.value)!==i.value&&n.children("li:eq("+Math.floor(i.value)+")").children("i").addClass(o).removeClass("layui-icon-rate-solid layui-icon-rate")})})},a.prototype.events=function(){this.config},l.render=function(e){e=new a(e);return function(){var a=this;return{setvalue:function(e){a.setvalue.call(a,e)},config:a.config}}.call(e)},e(i,l)});layui.define("jquery",function(l){"use strict";var g=layui.$,e=function(l){};e.prototype.load=function(l){var t,i,n,e,r,o,a,c,m,s,u,f,y,d=this,p=0,h=g((l=l||{}).elem);if(h[0])return e=g(l.scrollElem||document),r=l.mb||50,o=!("isAuto"in l)||l.isAuto,a=l.end||"\u6ca1\u6709\u66f4\u591a\u4e86",c=l.scrollElem&&l.scrollElem!==document,m="\u52a0\u8f7d\u66f4\u591a",s=g('"),h.find(".layui-flow-more")[0]||h.append(s),u=function(l,e){l=g(l),s.before(l),(e=0==e||null)?s.html(a):s.find("a").html(m),i=e,t=null,y&&y()},f=function(){t=!0,s.find("a").html(''),"function"==typeof l.done&&l.done(++p,u)},f(),s.find("a").on("click",function(){g(this);i||t||f()}),l.isLazyimg&&(y=d.lazyimg({elem:l.elem+" img",scrollElem:l.scrollElem})),o&&e.on("scroll",function(){var e=g(this),o=e.scrollTop();n&&clearTimeout(n),!i&&h.width()&&(n=setTimeout(function(){var l=(c?e:g(window)).height();(c?e.prop("scrollHeight"):document.documentElement.scrollHeight)-o-l<=r&&(t||f())},100))}),d},e.prototype.lazyimg=function(l){var e,c=this,m=0,s=g((l=l||{}).scrollElem||document),u=l.elem||"img",f=l.scrollElem&&l.scrollElem!==document,y=function(e,l){var o,t=s.scrollTop(),l=t+l,i=f?e.offset().top-s.offset().top+t:e.offset().top;t<=i&&i<=l&&e.attr("lay-src")&&(o=e.attr("lay-src"),layui.img(o,function(){var l=c.lazyimg.elem.eq(m);e.attr("src",o).removeAttr("lay-src"),l[0]&&n(l),m++},function(){c.lazyimg.elem.eq(m);e.removeAttr("lay-src")}))},n=function(l,e){var o=(f?e||s:g(window)).height(),t=s.scrollTop(),i=t+o;if(c.lazyimg.elem=g(u),l)y(l,o);else for(var n=0;n','
          '+e+"
          ",'
          ','',"
          ",""].join(""));return d.ie&&d.ie<8?s.removeClass("layui-hide").addClass("layui-show"):(c[0]&&c.remove(),f.call(a,o,s[0],n),s.addClass("layui-hide").after(o),a.index)},e.prototype.getContent=function(t){t=n(t);if(t[0])return l(t[0].document.body.innerHTML)},e.prototype.getText=function(t){t=n(t);if(t[0])return u(t[0].document.body).text()},e.prototype.setContent=function(t,e,i){var l=n(t);l[0]&&(i?u(l[0].document.body).append(e):u(l[0].document.body).html(e),layedit.sync(t))},e.prototype.sync=function(t){t=n(t);t[0]&&u("#"+t[1].attr("textarea")).val(l(t[0].document.body.innerHTML))},e.prototype.getSelection=function(t){var t=n(t);if(t[0])return t=p(t[0].document),document.selection?t.text:t.toString()},function(a,n,o){var s=this,r=a.find("iframe");r.css({height:o.height}).on("load",function(){var t=r.contents(),e=r.prop("contentWindow"),i=t.find("head"),l=u([""].join("")),t=t.find("body");i.append(l),t.attr("contenteditable","true").css({"min-height":o.height}).html(n.value||""),m.apply(s,[e,r,n,o]),g.call(s,e,a,o)})}),n=function(t){t=u("#LAY_layedit_"+t);return[t.prop("contentWindow"),t]},l=function(t){return t=8==d.ie?t.replace(/<.+>/g,function(t){return t.toLowerCase()}):t},m=function(e,t,i,l){var a=e.document,n=u(a.body);n.on("keydown",function(t){if(13===t.keyCode){var e=p(a);if("pre"===h(e).parentNode.tagName.toLowerCase())return t.shiftKey?void 0:(c.msg("\u8bf7\u6682\u65f6\u7528shift+enter"),!1);a.execCommand("formatBlock",!1,"

          ")}}),u(i).parents("form").on("submit",function(){var t=n.html();8==d.ie&&(t=t.replace(/<.+>/g,function(t){return t.toLowerCase()})),i.value=t}),n.on("paste",function(t){a.execCommand("formatBlock",!1,"

          "),setTimeout(function(){o.call(e,n),i.value=n.html()},100)})},o=function(t){this.document;t.find("*[style]").each(function(){var t=this.style.textAlign;this.removeAttribute("style"),u(this).css({"text-align":t||""})}),t.find("table").addClass("layui-table"),t.find("script,link").remove()},p=function(t){return t.selection?t.selection.createRange():t.getSelection().getRangeAt(0)},h=function(t){return t.endContainer||t.parentElement().childNodes[0]},v=function(t,e,i){var l,a,n=this.document,o=document.createElement(t);for(l in e)o.setAttribute(l,e[l]);o.removeAttribute("text"),n.selection?(a=i.text||e.text,"a"===t&&!a||(a&&(o.innerHTML=a),i.pasteHTML(u(o).prop("outerHTML")),i.select())):(a=i.toString()||e.text,"a"===t&&!a||(a&&(o.innerHTML=a),i.deleteContents(),i.insertNode(o)))},b=function(e,t){var i=this.document,l="layedit-tool-active",i=h(p(i)),a=function(t){return e.find(".layedit-tool-"+t)};t&&t[t.hasClass(l)?"removeClass":"addClass"](l),e.find(">i").removeClass(l),a("unlink").addClass(y),u(i).parents().each(function(){var t=this.tagName.toLowerCase(),e=this.style.textAlign;"b"!==t&&"strong"!==t||a("b").addClass(l),"i"!==t&&"em"!==t||a("i").addClass(l),"u"===t&&a("u").addClass(l),"strike"===t&&a("d").addClass(l),"p"===t&&a("center"===e?"center":"right"===e?"right":"left").addClass(l),"a"===t&&(a("link").addClass(l),a("unlink").removeClass(y))})},g=function(a,t,e){var n=a.document,o=u(n.body),s={link:function(i){var t=h(i),l=u(t).parent();x.call(o,{href:l.attr("href"),target:l.attr("target")},function(t){var e=l[0];"A"===e.tagName?e.href=t.url:v.call(a,"a",{target:t.target,href:t.url,text:t.url},i)})},unlink:function(t){n.execCommand("unlink")},code:function(e){k.call(o,function(t){v.call(a,"pre",{text:t.code,"lay-lang":t.lang},e)})},help:function(){c.open({type:2,title:"\u5e2e\u52a9",area:["600px","380px"],shadeClose:!0,shade:.1,skin:"layui-layer-msg",content:["","no"]})}},r=t.find(".layui-layedit-tool"),i=function(){var t,e=u(this),i=e.attr("layedit-event"),l=e.attr("lay-command");e.hasClass(y)||(o.focus(),(t=p(n)).commonAncestorContainer,l?(n.execCommand(l),/justifyLeft|justifyCenter|justifyRight/.test(l)&&n.execCommand("formatBlock",!1,"

          "),setTimeout(function(){o.focus()},10)):s[i]&&s[i].call(this,t),b.call(a,r,e))},l=/image/;r.find(">i").on("mousedown",function(){var t=u(this).attr("layedit-event");l.test(t)||i.call(this)}).on("click",function(){var t=u(this).attr("layedit-event");l.test(t)&&i.call(this)}),o.on("click",function(){b.call(a,r)})},x=function(t,i){var l=this,t=c.open({type:1,id:"LAY_layedit_link",area:"350px",shade:.05,shadeClose:!0,moveType:1,title:"\u8d85\u94fe\u63a5",skin:"layui-layer-msg",content:['

            ','
          • ','','
            ','',"
            ","
          • ",'
          • ','','
            ','",'","
            ","
          • ",'
          • ','','',"
          • ","
          "].join(""),success:function(t,e){a.render("radio"),t.find(".layui-btn-primary").on("click",function(){c.close(e),l.focus()}),a.on("submit(layedit-link-yes)",function(t){c.close(x.index),i&&i(t.field)})}});x.index=t},k=function(i){var l=this,t=c.open({type:1,id:"LAY_layedit_code",area:"550px",shade:.05,shadeClose:!0,moveType:1,title:"\u63d2\u5165\u4ee3\u7801",skin:"layui-layer-msg",content:['
            ','
          • ','','
            ','","
            ","
          • ",'
          • ','','
            ','',"
            ","
          • ",'
          • ','','',"
          • ","
          "].join(""),success:function(t,e){a.render("select"),t.find(".layui-btn-primary").on("click",function(){c.close(e),l.focus()}),a.on("submit(layedit-code-yes)",function(t){c.close(k.index),i&&i(t.field)})}});k.index=t},C={html:'',strong:'',italic:'',underline:'',del:'',"|":'',left:'',center:'',right:'',link:'',unlink:'',face:'',image:'',code:'',help:''},e=new e;t(i,e)});layui.define(["lay","util"],function(e){"use strict";var d=layui.$,o=layui.util,u="layui-code-title",l={elem:".layui-code",title:"</>",about:"",ln:!0};e("code",function(e){var c=e=d.extend({},l,e);e.elem=d(e.elem),e.elem[0]&&layui.each(e.elem.get().reverse(),function(e,l){var t,a=d(l),i=(i=a.html(),d.trim(i).replace(/^\n|\n$/,"")),l=d.extend({},c,lay.options(l),(t={},layui.each(["title","height","encode","skin","about"],function(e,l){var i=a.attr("lay-"+l);"string"==typeof i&&(t[l]=i)}),t)),s=l.ln?"ol":"ul",s=d("<"+s+' class="layui-code-'+s+'">'),n=d('
          ');a.addClass("layui-code-view layui-box"),l.skin&&("notepad"===l.skin&&(l.skin="dark"),a.addClass("layui-code-"+l.skin)),i=(i=l.encode?o.escape(i):i).replace(/[\r\t\n]+/g,"
        • "),a.html(s.html("
        • "+i+"
        • ")),a.children("."+u)[0]||(n.html(l.title+(l.about?'
          '+l.about+"
          ":"")),a.prepend(n)),0<(i=Math.floor(s.find("li").length/100))&&s.css("margin-left",i+"px"),l.height&&s.css("max-height",l.height)})})}).addcss("modules/code.css?v=3","skincodecss"); diff --git a/module/static/login/css/index.css b/module/static/login/css/index.css new file mode 100644 index 0000000..e2a523f --- /dev/null +++ b/module/static/login/css/index.css @@ -0,0 +1,97 @@ +* { + margin: 0; + padding: 0; +} + +.main { + position: relative; + width: 100%; + min-height: 100vh; +} +.main .content { + position: absolute; + top: 150px; + left: 50%; + transform: translateX(-50%); +} +.main .content .logo { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} +.main .content .logo img { + width: 150px; + height: 150px; +} +.main .content .logo .title { + margin-top: 40px; + font-size: 24px; +} +.main .content .logo .tips { + margin-top: 8px; + font-size: 16px; + color: #707579; +} +.main .content .password-input { + position: relative; + width: 408px; + margin-top: 48px; +} +.main .content .password-input:hover .tip { + color: #3390ec !important; +} +.main .content .password-input .input-p { + display: block; + box-sizing: border-box; + width: 100%; + height: 54px; + padding: 11px 13px; + border: 1px solid #dadce0; + border-radius: 12px; + font-size: 16px; + transition: transform 0.15s ease-out, color 0.15s ease-out; + outline: none; +} +.main .content .password-input .input-p:focus { + outline: 2px solid #3390ec; +} +.main .content .password-input .tip { + position: absolute; + left: 12px; + top: 15px; + padding: 0 4px; + background-color: #fff; + font-size: 16px; + font-weight: 400; + transition: transform 0.15s ease-out, color 0.15s ease-out; + transform-origin: left center; + color: #707579; + transform: scale(0.75) translate(-8px, -36px); +} +.main .content .confirm-btn { + margin-top: 44px; + outline: none !important; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 56px; + border: 0; + border-radius: 12px; + background-color: rgba(74, 149, 214, 0.08); + background-size: cover; + padding: 10px; + color: #3390ec; + line-height: 1.2; + cursor: pointer; + text-transform: uppercase; + flex-shrink: 0; + position: relative; + overflow: hidden; + transition: background-color 0.15s, color 0.15s; + text-decoration: none !important; + --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); +} + +/*# sourceMappingURL=index.css.map */ diff --git a/module/static/login/images/login.svg b/module/static/login/images/login.svg new file mode 100644 index 0000000..ae107ff --- /dev/null +++ b/module/static/login/images/login.svg @@ -0,0 +1 @@ + diff --git a/module/static/request/index.js b/module/static/request/index.js new file mode 100644 index 0000000..19dabdf --- /dev/null +++ b/module/static/request/index.js @@ -0,0 +1,20 @@ +var request = (url, type = 'get' | 'post', data) => { + const $ = layui.$ + + return new Promise((resolve, reject) => { + $.ajax({ + url, + type, + data, + dataType: 'json', + timeout: 60 * 1000, + contentType: 'application/x-www-form-urlencoded', + success: (res) => { + resolve(res) + }, + error: (err) => { + reject(err) + } + }) + }) +} diff --git a/module/templates/control.html b/module/templates/control.html new file mode 100644 index 0000000..97f7163 --- /dev/null +++ b/module/templates/control.html @@ -0,0 +1,1323 @@ + + + + + + Telegram 下载控制台 + + + +
          +
          +

          📥 Telegram 下载控制台

          +

          配置频道和时间范围,一键下载媒体文件

          +
          + +
          +
          当前配置
          +
          +
          +
          + 频道 + 加载中... +
          +
          + 时间过滤 + 加载中... +
          +
          + 保存路径 + 加载中... +
          +
          +
          + + +
          +
          +
          📜 历史频道
          + +
          +
          +
          +

          暂无历史记录

          +

          配置过的频道会显示在这里

          +
          +
          +
          + +
          +
          新建下载任务
          +
          +
          + +
          + + +
          +

          支持 t.me 链接、@用户名 或直接输入用户名

          +
          + + +
          +
          +
          📺
          +
          +

          频道名称

          +

          类型: 频道

          +
          +
          +
          + +
          +
          + + +
          +
          + + +

          留空表示下载到最新

          +
          +
          + +
          + + +
          +

          + 💡 建议先点击"验证频道"确认频道有效后,再保存配置 +

          +
          +
          + +
          +
          下载控制
          +
          + +
          +
          + 状态: 下载中 +
          +
          + +
          +
          📊 任务进度
          + + + +
          +
          +
          0 B/s
          +
          下载速度
          +
          +
          +
          0
          +
          下载中
          +
          +
          +
          0
          +
          已完成
          +
          +
          +
          0
          +
          已跳过
          +
          +
          + +
          +
          + + + + + +

          暂无下载任务

          +
          +
          +
          + +
          + Telegram Media Downloader v2.2.5 +
          + ✅ 点击"保存并重启下载"将自动保存配置并重启程序开始下载 +
          +
          + + + + diff --git a/module/templates/index.html b/module/templates/index.html new file mode 100644 index 0000000..a7e92c7 --- /dev/null +++ b/module/templates/index.html @@ -0,0 +1,1727 @@ + + + + + + Telegram 下载器 + + + + + +
          +
          +
          +
          ⚙ 过滤条件配置
          +
          + + +
          +
          + +
          + +
          +
          + +
          + 多个条件之间为 AND(且) 关系;勾选「排除」则对该条件取反(NOT) +
          +
          + + +
          + +
          + 可用字段:message_date、media_file_size、media_width、media_height、media_duration、media_file_name、message_caption
          + 操作符:>=、<=、>、<、==、!=、matches(正则)、contains(包含)、not(取反)
          + 示例:not media_file_name matches '.*\.txt$' +
          +
          + + +
          +
          生成的过滤表达式
          +
          (无过滤条件)
          + +
          +
          + +
          + + +
          + + +
          +
          +
          + + +
          +
          +
          +
          1
          API 凭证
          +
          2
          网络 & 路径
          +
          3
          确认
          +
          + + +
          +
          获取 Telegram API 凭证
          +
          连接 Telegram 需要你自己的 API 密钥,申请免费,约 1 分钟
          +
          +
          +
          1
          +
          + 打开官方开发者平台 +

          访问 my.telegram.org/apps,用 Telegram 账号登录。
          ⚠️ 验证码发到 Telegram App 内收件箱,不是短信

          +
          +
          +
          +
          2
          +
          + 创建应用 +

          填写任意名称,Platform 选 Desktop,点击「Create application」

          +
          +
          +
          +
          3
          +
          + 复制密钥到下方 +

          页面会显示 App api_idApp api_hash

          +
          +
          +
          +
          + + +
          +
          + + +
          +
          💡 遇到「too many tries」提示,等待 1~2 小时后重试
          +
          + + + + + + + +
          + +
          + +
          +
          +
          + + +
          +
          TG 下载器 v
          +
          +
          +
          0 B/s
          速度
          +
          0
          下载中
          +
          0
          已完成
          +
          0
          已跳过
          +
          +
          +
          连接中
          + + +
          +
          + + +
          + + +
          +
          频道
          +
          + + +
          +
          +
          +
          +
          +
          + + +
          +
          开始日期
          + +
          + + +
          +
          结束日期(可选)
          + +
          + + +
          +
          过滤条件
          + +
          + + +
          +
          + 保存路径 + +
          +
          + 加载中… +
          +
          + +
          + + +
          +
          +
          + + +
          + +
          + +
          + +
          + + +
          + + +
          +
          + + + + + +
          +
          当前任务
          +
          + + +
          +
          +
          + + 历史频道 +
          + +
          +
          +
          +
          暂无历史记录
          +
          +
          +
          + + +
          +
          + 正在下载 + 0 个文件 +
          +
          +
          + + + + + + 暂无下载任务 +
          +
          +
          + + +
          +
          + 已完成 + 0 个文件 +
          +
          +
          暂无已完成文件
          +
          + +
          + +
          + + + + diff --git a/module/templates/login.html b/module/templates/login.html new file mode 100644 index 0000000..b21d7c7 --- /dev/null +++ b/module/templates/login.html @@ -0,0 +1,68 @@ + + + + + + + login + + + + +
          +
          + +
          + +
          Your password
          +
          + +
          +
          + + + + + + + + diff --git a/module/templates/settings.html b/module/templates/settings.html new file mode 100644 index 0000000..fd60cf1 --- /dev/null +++ b/module/templates/settings.html @@ -0,0 +1,670 @@ + + + + + + 设置 — Telegram 下载器 + + + + + +
          + ← 返回主页 + | +
          ⚙ 设置
          +
          + +
          + + + + +
          + + + + + +
          + + +
          +
          +
          下载设置
          +
          +
          + + +
          同时下载的文件数,范围 1–20,修改后自动重启生效
          +
          +
          + + +
          修改后重启生效
          +
          +
          + + +
          +
          +
          +
          + +
          +
          + + +
          +
          +
          + 运行日志 + +
          +
          +
          + + + + +
          +
          加载中…
          + +
          +
          +
          + + +
          +
          +
          + 下载记录 + +
          +
          + +
          + + + + + + + + +
          ID频道消息ID文件名大小类型时间状态操作
          加载中…
          +
          + +
          +
          +
          + + +
          +
          +
          Session 状态
          +
          +
          检测中…
          +
          +
          + +
          +
          代理设置
          +
          +
          + + +
          +
          +
          + + +
          +
          + + +
          +
          + + + +
          +
          +
          留空地址则不使用代理。修改后自动重启。
          + +
          +
          + +
          +
          重新配置账户
          +
          +

          + 点击下方按钮将删除当前登录 session,服务重启后需在终端重新运行登录命令完成 Telegram 账号验证。
          + ⚠ 操作不可撤销,当前下载任务将被中断。 +

          + +
          +
          +
          + +
          +
          + + + + diff --git a/module/web.py b/module/web.py new file mode 100644 index 0000000..aa311b1 --- /dev/null +++ b/module/web.py @@ -0,0 +1,1167 @@ +"""web ui for media download""" + +import asyncio +import json +import logging +import os +import threading +from datetime import datetime +from typing import Callable, Optional + +import pyrogram +from flask import Flask, jsonify, render_template, request +from flask_login import LoginManager, UserMixin, login_required, login_user +from loguru import logger +from ruamel.yaml import YAML +from werkzeug.serving import make_server + +import utils +from module.app import Application, ChatDownloadConfig, TaskNode +from module.download_stat import ( + DownloadState, + get_download_result, + get_download_state, + get_total_download_speed, + get_task_progress, + set_download_state, + pause_message, + resume_message, + skip_message, + is_message_paused, + remove_download_entry, +) +from utils.crypto import AesBase64 +from utils.format import format_byte, replace_date_time + +# Config file path +CONFIG_FILE_PATH = "config.yaml" +CHANNEL_HISTORY_FILE = os.path.join(os.path.abspath("."), "appdata", "channel_history.json") + +log = logging.getLogger("werkzeug") +log.setLevel(logging.ERROR) + +_flask_app = Flask(__name__) + +_flask_app.secret_key = "tdl" +_login_manager = LoginManager() +_login_manager.login_view = "login" +_login_manager.init_app(_flask_app) +web_login_users: dict = {} +deAesCrypt = AesBase64("1234123412ABCDEF", "ABCDEF1234123412") + +# Global references for dynamic download +_app: Optional[Application] = None +_client: Optional[pyrogram.Client] = None +_add_download_task: Optional[Callable] = None +_download_chat_task: Optional[Callable] = None +_web_server = None # Reference to the web server for shutdown + + +class User(UserMixin): + """Web Login User""" + + def __init__(self): + self.sid = "root" + + @property + def id(self): + """ID""" + return self.sid + + +@_login_manager.user_loader +def load_user(_): + """ + Load a user object from the user ID. + + Returns: + User: The user object. + """ + return User() + + +def get_flask_app() -> Flask: + """get flask app instance""" + return _flask_app + + +# pylint: disable = W0603 +def init_web( + app: Application, + client: pyrogram.Client = None, + add_download_task: Callable = None, + download_chat_task: Callable = None, +): + """ + Set the value of the users variable. + + Args: + app: The application instance. + client: The pyrogram client instance. + add_download_task: Function to add download task. + download_chat_task: Function to download chat task. + + Returns: + None. + """ + global web_login_users, _app, _client, _add_download_task, _download_chat_task, _web_server + + _app = app + _client = client + _add_download_task = add_download_task + _download_chat_task = download_chat_task + + if app.web_login_secret: + web_login_users = {"root": app.web_login_secret} + else: + _flask_app.config["LOGIN_DISABLED"] = True + + # Create a stoppable server + _web_server = make_server(app.web_host, app.web_port, _flask_app, threaded=True) + + def run_server(): + _web_server.serve_forever() + + threading.Thread(target=run_server, daemon=True).start() + + +def shutdown_web(): + """Shutdown the web server""" + global _web_server + if _web_server: + logger.info("正在关闭 Web 服务器...") + _web_server.shutdown() + _web_server = None + + +@_flask_app.route("/login", methods=["GET", "POST"]) +def login(): + """ + Function to handle the login route. + + Parameters: + - No parameters + + Returns: + - If the request method is "POST" and the username and + password match the ones in the web_login_users dictionary, + it returns a JSON response with a code of "1". + - Otherwise, it returns a JSON response with a code of "0". + - If the request method is not "POST", it returns the rendered "login.html" template. + """ + if request.method == "POST": + username = "root" + web_login_form = {} + for key, value in request.form.items(): + if value: + value = deAesCrypt.decrypt(value) + web_login_form[key] = value + + if not web_login_form.get("password"): + return jsonify({"code": "0"}) + + password = web_login_form["password"] + if username in web_login_users and web_login_users[username] == password: + user = User() + login_user(user) + return jsonify({"code": "1"}) + + return jsonify({"code": "0"}) + + return render_template("login.html") + + +@_flask_app.route("/") +@login_required +def index(): + """Index html""" + return render_template( + "index.html", + download_state=( + "pause" if get_download_state() is DownloadState.Downloading else "continue" + ), + ) + + +@_flask_app.route("/get_download_status") +@login_required +def get_download_speed(): + """Get download speed and state""" + is_downloading = get_download_state() is DownloadState.Downloading + state = "pause" if is_downloading else "continue" + # 暂停状态时速度直接返回0 + speed = format_byte(get_total_download_speed()) if is_downloading else "0 B" + return ( + '{ "download_speed" : "' + + speed + + '/s" , "upload_speed" : "0.00 B/s", "state" : "' + state + '" }' + ) + + +@_flask_app.route("/api/task_progress") +@login_required +def api_task_progress(): + """Get current task progress including skipped files""" + progress = get_task_progress() + return jsonify(progress) + + +@_flask_app.route("/set_download_state", methods=["POST"]) +@login_required +def web_set_download_state(): + """Set download state""" + state = request.args.get("state") + + if state == "continue" and get_download_state() is DownloadState.StopDownload: + set_download_state(DownloadState.Downloading) + return "pause" + + if state == "pause" and get_download_state() is DownloadState.Downloading: + set_download_state(DownloadState.StopDownload) + return "continue" + + return state + + +@_flask_app.route("/api/message_control", methods=["POST"]) +@login_required +def api_message_control(): + """单条下载任务控制:暂停/继续/跳过""" + import module.database as db + data = request.get_json(silent=True) or {} + chat_id = str(data.get("chat_id", "")) + message_id = int(data.get("message_id", 0)) + action = data.get("action", "") + + if not chat_id or not message_id: + return jsonify({"success": False, "error": "缺少参数"}), 400 + + if action == "pause": + pause_message(chat_id, message_id) + elif action == "resume": + resume_message(chat_id, message_id) + elif action == "skip": + file_name = str(data.get("file_name", "")) + skip_message(chat_id, message_id) + remove_download_entry(chat_id, message_id) + db.record_skip(chat_id, "", message_id, file_name) + else: + return jsonify({"success": False, "error": "未知操作"}), 400 + + return jsonify({"success": True}) + + +@_flask_app.route("/api/undo_skip", methods=["POST"]) +@login_required +def api_undo_skip(): + """撤销手动跳过,删除数据库记录""" + import module.database as db + data = request.get_json(silent=True) or {} + chat_id = str(data.get("chat_id", "")) + message_id = int(data.get("message_id", 0)) + + if not chat_id or not message_id: + return jsonify({"success": False, "error": "缺少参数"}), 400 + + db.delete_record(chat_id, message_id) + return jsonify({"success": True}) + + +@_flask_app.route("/get_app_version") +def get_app_version(): + """Get telegram_media_downloader version""" + return utils.__version__ + + +@_flask_app.route("/get_download_list") +@login_required +def get_download_list(): + """get download list""" + import module.database as db + import json + + if request.args.get("already_down") is None: + return "[]" + + already_down = request.args.get("already_down") == "true" + + if already_down: + # 已完成列表从数据库读取,重启后不丢失,支持分页 + try: + page = max(1, int(request.args.get("page", 1))) + except (ValueError, TypeError): + page = 1 + try: + page_size = min(100, max(10, int(request.args.get("page_size", 50)))) + except (ValueError, TypeError): + page_size = 50 + offset = (page - 1) * page_size + records = db.get_recent_history(limit=page_size, offset=offset) + total = db.count_recent_history() + items = [] + for r in records: + items.append({ + "chat": r.get("chat_title") or r.get("chat_id", ""), + "chat_id": r.get("chat_id", ""), + "id": str(r.get("message_id", "")), + "filename": r.get("file_name", ""), + "total_size": format_byte(r.get("file_size") or 0), + "download_progress": "100", + "download_speed": r.get("download_time", ""), + "save_path": (r.get("file_path") or "").replace("\\", "/"), + "status": r.get("status", "success"), + }) + return json.dumps({ + "items": items, + "total": total, + "page": page, + "page_size": page_size, + "total_pages": max(1, (total + page_size - 1) // page_size), + }, ensure_ascii=False) + + download_result = get_download_result() + items = [] + for chat_id, messages in download_result.items(): + for idx, value in messages.items(): + is_already_down = value["down_byte"] == value["total_size"] + if is_already_down: + continue + download_speed = format_byte(value["download_speed"]) + "/s" + items.append({ + "chat": str(chat_id), + "id": str(idx), + "filename": os.path.basename(value["file_name"]), + "total_size": format_byte(value["total_size"]), + "download_progress": str(round(value["down_byte"] / value["total_size"] * 100, 1)), + "download_speed": download_speed, + "save_path": value["file_name"].replace("\\", "/"), + "paused": is_message_paused(str(chat_id), idx), + }) + return json.dumps(items, ensure_ascii=False) + + +@_flask_app.route("/control") +@login_required +def control_page(): + """Control page - redirects to unified index page""" + return render_template( + "index.html", + download_state=( + "pause" if get_download_state() is DownloadState.Downloading else "continue" + ), + ) + + +@_flask_app.route("/api/setup_status") +def api_setup_status(): + """检测配置完成度,无需登录,新用户引导使用""" + try: + yaml_inst = YAML() + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml_inst.load(f) + + api_id = config.get("api_id", 0) or 0 + api_hash = str(config.get("api_hash", "") or "") + + has_api_credentials = ( + bool(api_id) and str(api_id) != "0" + and bool(api_hash) and len(api_hash) >= 8 + and api_hash not in ("your_api_hash", "YOUR_API_HASH", "") + ) + + session_paths = [ + os.path.join("sessions", "media_downloader.session"), + "media_downloader.session", + ] + has_session = any(os.path.exists(p) for p in session_paths) + + chats = config.get("chat", []) or [] + has_chat = bool(chats and chats[0].get("chat_id")) + + proxy = dict(config.get("proxy", {}) or {}) + + return jsonify({ + "has_api_credentials": has_api_credentials, + "has_session": has_session, + "has_chat": has_chat, + "proxy": proxy, + "save_path": config.get("save_path", ""), + }) + except Exception as e: + logger.exception(f"setup_status error: {e}") + return jsonify({"has_api_credentials": False, "error": str(e)}) + + +@_flask_app.route("/api/save_initial_config", methods=["POST"]) +def api_save_initial_config(): + """保存初始配置(API 凭证 + 代理 + 路径),无需登录""" + global _app + try: + data = request.get_json() + api_id_raw = data.get("api_id", "") + api_hash = str(data.get("api_hash", "")).strip() + save_path = str(data.get("save_path", "")).strip() + proxy_enabled = bool(data.get("proxy_enabled", False)) + proxy_scheme = str(data.get("proxy_scheme", "socks5")).strip() + proxy_hostname = str(data.get("proxy_hostname", "")).strip() + proxy_port_raw = data.get("proxy_port", 7891) + + try: + api_id = int(api_id_raw) + except (ValueError, TypeError): + return jsonify({"success": False, "error": "api_id 必须是数字"}) + + try: + proxy_port = int(proxy_port_raw) + except (ValueError, TypeError): + proxy_port = 7891 + + if not api_id or not api_hash: + return jsonify({"success": False, "error": "api_id 和 api_hash 不能为空"}) + + yaml_inst = YAML() + yaml_inst.preserve_quotes = True + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml_inst.load(f) + + config["api_id"] = api_id + config["api_hash"] = api_hash + + if save_path: + config["save_path"] = save_path + + if proxy_enabled and proxy_hostname: + config["proxy"] = { + "scheme": proxy_scheme, + "hostname": proxy_hostname, + "port": proxy_port, + } + elif not proxy_enabled and "proxy" in config: + del config["proxy"] + + with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f: + yaml_inst.dump(config, f) + + if _app: + _app.restart_program = True + + return jsonify({"success": True, "message": "配置已保存,程序即将重启"}) + except Exception as e: + logger.exception(f"save_initial_config error: {e}") + return jsonify({"success": False, "error": str(e)}) + + +@_flask_app.route("/api/get_config") +@login_required +def api_get_config(): + """Get current configuration""" + try: + yaml = YAML() + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml.load(f) + + chat_config = config.get("chat", [{}])[0] if config.get("chat") else {} + return jsonify({ + "chat_id": chat_config.get("chat_id", ""), + "download_filter": chat_config.get("download_filter", ""), + "save_path": config.get("save_path", "") + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +def _build_download_filter(start_date: str, end_date: str) -> str: + """Build download filter string from date range""" + if not start_date: + return "" + + download_filter = f"message_date >= {start_date} 00:00:00" + if end_date: + download_filter += f" and message_date <= {end_date} 23:59:59" + return download_filter + + +# ============ Channel History Management ============ + +def _load_channel_history() -> list: + """Load channel history from file""" + try: + if os.path.exists(CHANNEL_HISTORY_FILE): + with open(CHANNEL_HISTORY_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + logger.warning(f"Failed to load channel history: {e}") + return [] + + +def _save_channel_history(history: list): + """Save channel history to file""" + try: + with open(CHANNEL_HISTORY_FILE, "w", encoding="utf-8") as f: + json.dump(history, f, ensure_ascii=False, indent=2) + except Exception as e: + logger.warning(f"Failed to save channel history: {e}") + + +def _add_to_channel_history(chat_id: str, chat_title: str, chat_type: str = ""): + """Add or update a channel in history""" + history = _load_channel_history() + + # Check if already exists, update if so + for item in history: + if item.get("chat_id") == chat_id: + item["chat_title"] = chat_title or item.get("chat_title", chat_id) + item["chat_type"] = chat_type or item.get("chat_type", "") + item["last_used"] = datetime.now().isoformat() + item["use_count"] = item.get("use_count", 0) + 1 + break + else: + # Not found, add new + history.insert(0, { + "chat_id": chat_id, + "chat_title": chat_title or chat_id, + "chat_type": chat_type, + "last_used": datetime.now().isoformat(), + "use_count": 1 + }) + + # Sort by last_used (most recent first) and limit to 20 items + history.sort(key=lambda x: x.get("last_used", ""), reverse=True) + history = history[:20] + + _save_channel_history(history) + + +def _remove_from_channel_history(chat_id: str) -> bool: + """Remove a channel from history""" + history = _load_channel_history() + original_len = len(history) + history = [item for item in history if item.get("chat_id") != chat_id] + + if len(history) < original_len: + _save_channel_history(history) + return True + return False + + +@_flask_app.route("/api/channel_history") +@login_required +def api_get_channel_history(): + """Get channel history list""" + try: + history = _load_channel_history() + return jsonify({"success": True, "history": history}) + except Exception as e: + logger.exception(f"Error getting channel history: {e}") + return jsonify({"success": False, "error": str(e)}) + + +@_flask_app.route("/api/channel_history/", methods=["DELETE"]) +@login_required +def api_delete_channel_history(chat_id: str): + """Delete a channel from history""" + try: + if _remove_from_channel_history(chat_id): + return jsonify({"success": True, "message": "已删除"}) + return jsonify({"success": False, "error": "未找到该频道"}) + except Exception as e: + logger.exception(f"Error deleting channel history: {e}") + return jsonify({"success": False, "error": str(e)}) + + +@_flask_app.route("/api/channel_history/clear", methods=["POST"]) +@login_required +def api_clear_channel_history(): + """Clear all channel history""" + try: + _save_channel_history([]) + return jsonify({"success": True, "message": "历史记录已清空"}) + except Exception as e: + logger.exception(f"Error clearing channel history: {e}") + return jsonify({"success": False, "error": str(e)}) + + +def _update_chat_config(chat_id: str, download_filter: str, update_memory: bool = False) -> dict: + """ + Unified function to update chat configuration. + + Args: + chat_id: The chat/channel ID + download_filter: The download filter string + update_memory: Whether to also update in-memory config + + Returns: + dict with keys: success, error (if failed), chat_title (if available) + """ + global _app + + try: + # Read current config + yaml = YAML() + yaml.preserve_quotes = True + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml.load(f) + + # Update chat config in file + if "chat" not in config or not config["chat"]: + config["chat"] = [{}] + + config["chat"][0]["chat_id"] = chat_id + config["chat"][0]["last_read_message_id"] = 0 + + if download_filter: + config["chat"][0]["download_filter"] = download_filter + elif "download_filter" in config["chat"][0]: + del config["chat"][0]["download_filter"] + + # Write config to file + with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f: + yaml.dump(config, f) + + # Update in-memory config if requested + if update_memory and _app: + # Update _app.config + if "chat" not in _app.config or not _app.config["chat"]: + _app.config["chat"] = [{}] + + _app.config["chat"][0]["chat_id"] = chat_id + _app.config["chat"][0]["last_read_message_id"] = 0 + + if download_filter: + _app.config["chat"][0]["download_filter"] = download_filter + elif "download_filter" in _app.config["chat"][0]: + del _app.config["chat"][0]["download_filter"] + + # Clear old chat_download_config and add new one + _app.chat_download_config.clear() + new_config = ChatDownloadConfig() + new_config.last_read_message_id = 0 + new_config.download_filter = download_filter + _app.chat_download_config[chat_id] = new_config + + return {"success": True} + + except Exception as e: + logger.exception(f"Error updating config: {e}") + return {"success": False, "error": str(e)} + + +async def _validate_chat(chat_id: str) -> dict: + """ + Validate chat/channel and get its info. + + Returns: + dict with keys: valid, chat_id, chat_title, error (if invalid) + """ + global _client + + if not _client: + return {"valid": False, "error": "Telegram 客户端未就绪"} + + try: + chat = await _client.get_chat(chat_id) + return { + "valid": True, + "chat_id": str(chat.id), + "chat_title": chat.title or chat.first_name or chat_id, + "chat_type": str(chat.type).split(".")[-1] if chat.type else "unknown" + } + except pyrogram.errors.exceptions.bad_request_400.UsernameNotOccupied: + return {"valid": False, "error": f"频道/群组 '{chat_id}' 不存在"} + except pyrogram.errors.exceptions.bad_request_400.PeerIdInvalid: + return {"valid": False, "error": f"无效的频道/群组 ID: {chat_id}"} + except Exception as e: + return {"valid": False, "error": f"验证失败: {str(e)}"} + + +@_flask_app.route("/api/validate_chat", methods=["POST"]) +@login_required +def api_validate_chat(): + """Validate chat/channel and return its info""" + global _app + + try: + data = request.get_json() + chat_id = data.get("chat_id", "").strip() + + if not chat_id: + return jsonify({"valid": False, "error": "频道ID不能为空"}) + + if not _app or not _client: + return jsonify({"valid": False, "error": "服务未就绪,请稍后再试"}) + + # Run validation in event loop + future = asyncio.run_coroutine_threadsafe(_validate_chat(chat_id), _app.loop) + result = future.result(timeout=10) + + return jsonify(result) + + except asyncio.TimeoutError: + return jsonify({"valid": False, "error": "验证超时,请检查网络连接"}) + except Exception as e: + logger.exception(f"Error validating chat: {e}") + return jsonify({"valid": False, "error": str(e)}) + + +@_flask_app.route("/api/save_path", methods=["POST"]) +@login_required +def api_save_path(): + """修改文件保存路径""" + try: + data = request.get_json() + save_path = str(data.get("save_path", "")).strip() + if not save_path: + return jsonify({"success": False, "error": "路径不能为空"}) + + yaml_inst = YAML() + yaml_inst.preserve_quotes = True + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml_inst.load(f) + + config["save_path"] = save_path + + with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f: + yaml_inst.dump(config, f) + + # 同步更新内存中的配置 + if _app: + _app.save_path = save_path + + return jsonify({"success": True, "save_path": save_path}) + except Exception as e: + logger.exception(f"save_path error: {e}") + return jsonify({"success": False, "error": str(e)}) + + +@_flask_app.route("/api/set_config", methods=["POST"]) +@login_required +def api_set_config(): + """Set download configuration (file only, no restart)""" + try: + data = request.get_json() + chat_id = data.get("chat_id", "").strip() + start_date = data.get("start_date", "").strip() + end_date = data.get("end_date", "").strip() + + if not chat_id: + return jsonify({"success": False, "error": "频道ID不能为空"}) + + download_filter = _build_download_filter(start_date, end_date) + result = _update_chat_config(chat_id, download_filter, update_memory=False) + + return jsonify(result) + + except Exception as e: + logger.exception(f"Error in api_set_config: {e}") + return jsonify({"success": False, "error": str(e)}) + + +@_flask_app.route("/api/save_and_restart", methods=["POST"]) +@login_required +def api_save_and_restart(): + """Save configuration and restart the program""" + global _app + + try: + data = request.get_json() + chat_id = data.get("chat_id", "").strip() + start_date = data.get("start_date", "").strip() + end_date = data.get("end_date", "").strip() + chat_title = data.get("chat_title", "") # Optional, from validation + chat_type = data.get("chat_type", "") # Optional, from validation + + if not chat_id: + return jsonify({"success": False, "error": "频道ID不能为空"}) + + # 优先使用前端传来的完整过滤表达式,否则从日期范围构建 + custom_filter = data.get("download_filter", "").strip() + if custom_filter: + download_filter = custom_filter + else: + download_filter = _build_download_filter(start_date, end_date) + + # Update both file and memory + result = _update_chat_config(chat_id, download_filter, update_memory=True) + + if not result.get("success"): + return jsonify(result) + + # Add to channel history + _add_to_channel_history(chat_id, chat_title, chat_type) + + # Trigger restart + if _app: + _app.restart_program = True + logger.info(f"Restart flag set, new chat_id: {chat_id} ({chat_title})") + + return jsonify({ + "success": True, + "message": f"配置已保存,正在重启下载 {chat_title or chat_id}..." + }) + + except Exception as e: + logger.exception(f"Error in api_save_and_restart: {e}") + return jsonify({"success": False, "error": str(e)}) + + +@_flask_app.route("/api/start_download", methods=["POST"]) +@login_required +def api_start_download(): + """Start download task dynamically without restart""" + global _app, _client, _download_chat_task + + if not _client or not _download_chat_task or not _app: + return jsonify({ + "success": False, + "error": "下载服务未就绪,请稍后再试" + }) + + try: + data = request.get_json() + chat_id = data.get("chat_id", "").strip() + start_date = data.get("start_date", "").strip() + end_date = data.get("end_date", "").strip() + + if not chat_id: + return jsonify({"success": False, "error": "频道ID不能为空"}) + + # 优先使用前端传来的完整过滤表达式 + custom_filter = data.get("download_filter", "").strip() + if custom_filter: + download_filter = custom_filter + else: + download_filter = _build_download_filter(start_date, end_date) + if download_filter: + download_filter = replace_date_time(download_filter) + + # Create download config + chat_download_config = ChatDownloadConfig() + chat_download_config.last_read_message_id = 0 + chat_download_config.download_filter = download_filter + + # Create task node + node = TaskNode(chat_id=chat_id) + + # Also update the app's chat_download_config for persistence + _app.chat_download_config[chat_id] = chat_download_config + + # Start download task in the event loop + async def start_task(): + try: + chat = await _client.get_chat(chat_id) + logger.info(f"Starting download from web UI: {chat.title or chat_id}") + await _download_chat_task(_client, chat_download_config, node) + except Exception as e: + logger.error(f"Error starting download task: {e}") + raise e + + # Schedule the task + future = asyncio.run_coroutine_threadsafe(start_task(), _app.loop) + + # Wait a short time to check if task started successfully + try: + future.result(timeout=5) + except asyncio.TimeoutError: + # Task is running, which is expected + pass + except Exception as e: + return jsonify({ + "success": False, + "error": f"启动下载任务失败: {str(e)}" + }) + + return jsonify({ + "success": True, + "message": f"下载任务已启动: {chat_id}" + }) + + except Exception as e: + logger.exception(f"Error in api_start_download: {e}") + return jsonify({"success": False, "error": str(e)}) + + +# ============ 代理连通性测试 ============ + +@_flask_app.route("/api/test_proxy", methods=["POST"]) +def api_test_proxy(): + """测试代理连通性,无需登录,向导配置时使用""" + import time as _time + import requests as _req + + data = request.get_json() or {} + scheme = str(data.get("scheme", "")).strip() + hostname = str(data.get("hostname", "")).strip() + port_raw = data.get("port", 0) + + # 若未传参数,从配置文件读取 + if not hostname or not port_raw: + try: + yaml_inst = YAML() + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml_inst.load(f) + proxy_cfg = config.get("proxy", {}) or {} + scheme = scheme or str(proxy_cfg.get("scheme", "http")) + hostname = hostname or str(proxy_cfg.get("hostname", "")) + port_raw = port_raw or proxy_cfg.get("port", 0) + except Exception as e: + return jsonify({"success": False, "error": f"读取配置失败:{e}"}) + + if not hostname or not port_raw: + return jsonify({"success": False, "error": "未配置代理地址或端口"}) + + try: + port = int(port_raw) + except (ValueError, TypeError): + return jsonify({"success": False, "error": "端口号必须是数字"}) + + proxy_url = f"{scheme}://{hostname}:{port}" + proxies = {"http": proxy_url, "https": proxy_url} + + try: + t0 = _time.time() + resp = _req.get( + "https://api.telegram.org", + proxies=proxies, + timeout=8, + allow_redirects=True, + ) + latency = round((_time.time() - t0) * 1000) + return jsonify({ + "success": True, + "latency": latency, + "message": f"代理连接成功,延迟 {latency} ms", + }) + except _req.exceptions.ProxyError: + return jsonify({"success": False, "error": "代理认证失败或协议不匹配,请检查代理类型(socks5/http)"}) + except _req.exceptions.ConnectionError: + return jsonify({"success": False, "error": f"代理地址不可达({hostname}:{port}),请确认代理软件已启动"}) + except _req.exceptions.Timeout: + return jsonify({"success": False, "error": "代理连接超时(8秒),节点可能不可用,请切换节点后重试"}) + except Exception as e: + return jsonify({"success": False, "error": f"测试失败:{str(e)}"}) + + +# ============ 过滤器语法校验 ============ + +# 字段名 → 中文名映射,用于错误提示 +_FILTER_FIELD_NAMES = { + "message_date": "消息日期", + "message_id": "消息ID", + "message_caption": "消息说明", + "media_file_size": "文件大小", + "media_width": "视频宽度", + "media_height": "视频高度", + "media_file_name": "文件名", + "media_duration": "视频时长", + "sender_id": "发送者ID", + "sender_name": "发送者名称", + "reply_to_message_id": "回复消息ID", +} + +_VALID_FILTER_FIELDS = set(_FILTER_FIELD_NAMES.keys()) | { + "id", "caption", "file_size", "file_name", "media_type", + "file_extension", "message_thread_id", "topic_id", +} + + +def _localize_filter_error(error: str) -> str: + """将过滤器错误信息转为中文友好提示""" + if not error: + return error + if "Undefined name" in error: + # 提取字段名 + import re as _re + m = _re.search(r"Undefined name['\s]+([a-zA-Z_][a-zA-Z0-9_]*)", error) + field = m.group(1) if m else "" + valid = "、".join(list(_FILTER_FIELD_NAMES.keys())[:6]) + " 等" + return f"未知字段「{field}」,可用字段:{valid}" + if "Syntax error at EOF" in error: + return "表达式不完整,请检查结尾是否缺少值" + if "Syntax error at" in error: + import re as _re + m = _re.search(r"Syntax error at '(.+?)'", error) + token = m.group(1) if m else "" + return f"语法错误:「{token}」位置不正确,请检查操作符或引号" + if "is str but" in error or "is int but" in error or "is datetime but" in error: + return f"类型不匹配:{error}(字符串字段请用单引号括起来,如 'abc')" + return error + + +@_flask_app.route("/api/validate_filter", methods=["POST"]) +@login_required +def api_validate_filter(): + """校验过滤器表达式语法,返回 {valid, error}""" + from datetime import datetime as _dt + from module.filter import Filter + from utils.meta_data import MetaData + + try: + data = request.get_json() or {} + filter_str = (data.get("filter") or "").strip() + + if not filter_str: + return jsonify({"valid": True, "error": ""}) + + # 用带类型值的虚拟元数据执行校验(确保类型检查也能通过) + dummy = MetaData( + message_date=_dt(2024, 1, 1), + message_id=1, + message_caption="test caption", + media_file_size=1024 * 1024 * 10, + media_width=1920, + media_height=1080, + media_file_name="test_video.mp4", + media_duration=120, + sender_id=123456, + sender_name="TestUser", + reply_to_message_id=0, + ) + f = Filter() + f.set_meta_data(dummy) + valid, error = f.check_filter(filter_str) + + return jsonify({ + "valid": valid, + "error": _localize_filter_error(error) if error else "", + }) + + except Exception as e: + return jsonify({"valid": False, "error": _localize_filter_error(str(e))}) + + +# ============ 设置页面 ============ + +@_flask_app.route("/settings") +@login_required +def settings_page(): + """设置页面""" + return render_template("settings.html") + + +@_flask_app.route("/api/settings/general", methods=["GET", "POST"]) +@login_required +def api_settings_general(): + """读取或保存常规设置(并发数、日志级别、保存路径、代理)""" + yaml_inst = YAML() + yaml_inst.preserve_quotes = True + + if request.method == "GET": + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml_inst.load(f) + proxy = config.get("proxy") or {} + return jsonify({ + "max_download_task": config.get("max_download_task", 5), + "log_level": config.get("log_level", "INFO"), + "save_path": config.get("save_path", ""), + "proxy": { + "scheme": proxy.get("scheme", "socks5"), + "hostname": proxy.get("hostname", ""), + "port": proxy.get("port", 7890), + }, + }) + + # POST — 写入配置并重启 + data = request.get_json(silent=True) or {} + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config = yaml_inst.load(f) + + if "max_download_task" in data: + v = int(data["max_download_task"]) + if not (1 <= v <= 20): + return jsonify({"success": False, "error": "并发数须在 1-20 之间"}) + config["max_download_task"] = v + if "log_level" in data: + config["log_level"] = str(data["log_level"]) + if "save_path" in data and data["save_path"]: + config["save_path"] = str(data["save_path"]) + if "proxy" in data: + p = data["proxy"] + if p.get("hostname"): + config["proxy"] = { + "scheme": p.get("scheme", "socks5"), + "hostname": p["hostname"], + "port": int(p.get("port", 7890)), + } + else: + config.pop("proxy", None) + + with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f: + yaml_inst.dump(config, f) + + if _app: + _app.restart_program = True + return jsonify({"success": True}) + + +@_flask_app.route("/api/settings/logs") +@login_required +def api_settings_logs(): + """读取运行日志,支持分页和过滤""" + page = max(1, int(request.args.get("page", 1))) + page_size = min(500, max(10, int(request.args.get("page_size", 100)))) + level_filter = request.args.get("level", "").upper() + keyword = request.args.get("keyword", "").strip() + + log_path = os.path.join(os.path.abspath("."), "log", "tdl.log") + if not os.path.exists(log_path): + return jsonify({"lines": [], "total": 0, "page": 1, "pages": 0}) + + with open(log_path, "r", encoding="utf-8", errors="replace") as f: + all_lines = f.readlines() + + # 倒序,最新在前 + all_lines = [l.rstrip("\n") for l in reversed(all_lines) if l.strip()] + + # 过滤 + if level_filter and level_filter != "ALL": + all_lines = [l for l in all_lines if f"| {level_filter}" in l or f"|{level_filter}" in l] + if keyword: + kw_lower = keyword.lower() + all_lines = [l for l in all_lines if kw_lower in l.lower()] + + total = len(all_lines) + pages = max(1, (total + page_size - 1) // page_size) + start = (page - 1) * page_size + lines = all_lines[start: start + page_size] + + return jsonify({"lines": lines, "total": total, "page": page, "pages": pages}) + + +@_flask_app.route("/api/settings/db_records") +@login_required +def api_settings_db_records(): + """查询数据库下载记录,支持分页和多条件过滤""" + import module.database as db + page = max(1, int(request.args.get("page", 1))) + page_size = min(200, max(10, int(request.args.get("page_size", 50)))) + offset = (page - 1) * page_size + + records, total = db.query_records( + chat_id=request.args.get("chat_id", ""), + file_name=request.args.get("file_name", ""), + status=request.args.get("status", ""), + media_type=request.args.get("media_type", ""), + date_from=request.args.get("date_from", ""), + date_to=request.args.get("date_to", ""), + limit=page_size, + offset=offset, + ) + pages = max(1, (total + page_size - 1) // page_size) + return jsonify({"records": records, "total": total, "page": page, "pages": pages}) + + +@_flask_app.route("/api/settings/clear_session", methods=["POST"]) +@login_required +def api_settings_clear_session(): + """删除 session 文件并重启,用于切换 TG 账户""" + session_path = os.path.join( + os.path.abspath("."), "appdata", "sessions", "media_downloader.session" + ) + if os.path.exists(session_path): + try: + os.remove(session_path) + except Exception as e: + return jsonify({"success": False, "error": f"删除 session 失败: {e}"}) + if _app: + _app.restart_program = True + return jsonify({"success": True}) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..f96d433 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +warn_return_any = True + +[mypy-yaml.*] +ignore_missing_imports = True + +[mypy-tests.*] +ignore_errors = True diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..14a68cb --- /dev/null +++ b/pylintrc @@ -0,0 +1,509 @@ +# pylint-version: 2.2 + +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + pycurl, + cdecimal, + + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS .git + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=no + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable= + locally-disabled, + file-ignored, + fixme, + useless-object-inheritance, + + redefined-variable-type, + redefined-argument-from-local, + wrong-import-position, + consider-using-ternary, + redefined-outer-name, + + invalid-name, + bad-continuation, + import-error, + broad-except, + + unspecified-encoding, + + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=no + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, XXX, TODO + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes= + st.config._config._section._unset, + + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=90 + +# Maximum number of lines in a module. +max-module-lines= + 1500 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=(?x) + (_|dummy)$ + + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=(?x) + _|(?:dummy|(?:kw)?args|request|response|context|ctx)$ + + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, bar, baz, toto, tutu, tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +const-rgx=(?x)( + ([A-Z_][A-Z0-9_]*) + |(__.*__) + |(.+_)?logger + |(.+_)?predicate + |t_[a-z0-9]+(_[a-z0-9]+)* + |(.*_)?templates + )$ + + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Good variable names which should always be accepted, separated by a comma. +good-names= + _, j, db, e, fd, fp, + + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(?x)( + __.*__ + |test_.* + |.+Test + |render_.+ + |repeat_.+ + |(?:Pre)?Render + )$ + + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=regsub, TERMIOS, Bastion, rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods= + __init__, + __new__, + setUp, + + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, _fields, _replace, _source, _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=10 + +# Maximum number of attributes for a class (see R0902). +max-attributes=8 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=13 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception". +overgeneral-exceptions=Exception diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6d0288c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +#https://github.com/tangyoha/pyrogram/archive/refs/tags/v2.0.69.zip +https://github.com/tangyoha/pyrogram/archive/refs/heads/patch.zip +#https://github.com/tangyoha/pyrogram/archive/refs/heads/master_v2.0.69_1.zip +PyYAML==5.3.1 +rich==12.5.1 +PyTgCrypto==1.2.12 +loguru==0.6.0 +# aligo==5.4.0 +Werkzeug==2.2.2 +flask==2.2.2 +ply==3.11 +ruamel.yaml==0.17.21 +flask-login==0.6.2 +pycryptodome==3.18.0 +requests==2.32.3 diff --git a/screenshot/alipay.JPG b/screenshot/alipay.JPG new file mode 100644 index 0000000..e8aa0d9 Binary files /dev/null and b/screenshot/alipay.JPG differ diff --git a/screenshot/bot.gif b/screenshot/bot.gif new file mode 100644 index 0000000..770e2d9 Binary files /dev/null and b/screenshot/bot.gif differ diff --git a/screenshot/web_ui.gif b/screenshot/web_ui.gif new file mode 100644 index 0000000..e75009b Binary files /dev/null and b/screenshot/web_ui.gif differ diff --git a/screenshot/wechat.JPG b/screenshot/wechat.JPG new file mode 100644 index 0000000..4a0990d Binary files /dev/null and b/screenshot/wechat.JPG differ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..172ffe5 --- /dev/null +++ b/setup.py @@ -0,0 +1,42 @@ +from distutils.core import setup + +from utils import __version__ + +setup( + name="telegram-media-downloader", + version=__version__, + author="tangyoha", + author_email="tangyoha@outlook.com", + description="A simple script to download media from telegram", + url="https://github.com/tangyoha/telegram_media_downloader", + download_url="https://github.com/tangyoha/telegram_media_downloader/releases/latest", + py_modules=["media_downloader"], + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Internet", + "Topic :: Communications", + "Topic :: Communications :: Chat", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + project_urls={ + "Tracker": "https://github.com/tangyoha/telegram_media_downloader/issues", + "Community": "https://t.me/TeegramMediaDownload", + "Source": "https://github.com/tangyoha/telegram_media_downloader", + }, + python_requires="~=3.7", +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/module/__init__.py b/tests/module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/module/test_app.py b/tests/module/test_app.py new file mode 100644 index 0000000..5b09154 --- /dev/null +++ b/tests/module/test_app.py @@ -0,0 +1,71 @@ +"""test app""" + +import os +import sys +import unittest +from unittest import mock + +import module.app +from module.app import Application, ChatDownloadConfig, DownloadStatus + +sys.path.append("..") # Adds higher directory to python modules path. + + +class AppTestCase(unittest.TestCase): + @classmethod + def tearDownClass(cls): + config_test = os.path.join(os.path.abspath("."), "config_test.yaml") + data_test = os.path.join(os.path.abspath("."), "data_test.yaml") + if os.path.exists(config_test): + os.remove(config_test) + if os.path.exists(data_test): + os.remove(data_test) + + def test_app(self): + app = Application("", "") + self.assertEqual(app.save_path, os.path.join(os.path.abspath("."), "downloads")) + self.assertEqual(app.proxy, {}) + self.assertEqual(app.restart_program, False) + + app.chat_download_config[123] = ChatDownloadConfig() + app.chat_download_config[123].last_read_message_id = 13 + app.chat_download_config[123].node.download_status[ + 6 + ] = DownloadStatus.Downloading + app.chat_download_config[123].ids_to_retry.append(7) + # download success + app.chat_download_config[123].node.download_status[ + 8 + ] = DownloadStatus.SuccessDownload + app.chat_download_config[123].finish_task += 1 + # download success + app.chat_download_config[123].node.download_status[ + 10 + ] = DownloadStatus.SuccessDownload + app.chat_download_config[123].finish_task += 1 + # not exist message + app.chat_download_config[123].node.download_status[ + 13 + ] = DownloadStatus.SuccessDownload + app.config["chat"] = [{"chat_id": 123, "last_read_message_id": 5}] + + app.update_config(False) + + self.assertEqual( + app.chat_download_config[123].last_read_message_id + 1, + app.config["chat"][0]["last_read_message_id"], + ) + self.assertEqual( + [6, 7], + app.app_data["chat"][0]["ids_to_retry"], + ) + + @mock.patch("__main__.__builtins__.open", new_callable=mock.mock_open) + @mock.patch("module.app.yaml", autospec=True) + def test_update_config(self, mock_yaml, mock_open): + app = Application("", "") + app.config_file = "config_test.yaml" + app.app_data_file = "data_test.yaml" + app.config["chat"] = [{"chat_id": 123, "last_read_message_id": 0}] + app.update_config() + mock_open.assert_called_with("data_test.yaml", "w", encoding="utf-8") diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 0000000..17764c7 --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,203 @@ +import datetime +import platform + +from pyrogram.file_id import PHOTO_TYPES, FileType + + +class Chat: + def __init__(self, chat_id, chat_title): + self.id = chat_id + self.title = chat_title + + +class Date: + def __init__(self, date): + self.date = date + + def strftime(self, str) -> str: + return "" + + +class MockMessage: + def __init__(self, **kwargs): + self.id = kwargs.get("id") + self.media = kwargs.get("media") + self.audio = kwargs.get("audio", None) + self.document = kwargs.get("document", None) + self.photo = kwargs.get("photo", None) + self.video = kwargs.get("video", None) + self.voice = kwargs.get("voice", None) + self.video_note = kwargs.get("video_note", None) + self.media_group_id = kwargs.get("media_group_id", None) + self.caption = kwargs.get("caption", None) + self.text = kwargs.get("text", None) + self.empty = kwargs.get("empty", False) + self.from_user = kwargs.get("from_user", None) + self.reply_to_message_id = kwargs.get("reply_to_message_id", None) + self.caption_entities = kwargs.get("caption_entities", None) + + if kwargs.get("dis_chat") == None: + self.chat = Chat( + kwargs.get("chat_id", None), kwargs.get("chat_title", None) + ) + else: + self.chat = kwargs.get("chat", None) + self.date: datetime = None + if kwargs.get("date") != None: + self.date = kwargs["date"] + + +class MockUser: + def __init__(self, **kwargs): + self.id = kwargs.get("id", 0) + self.username = kwargs.get("username", "") + + +class MockAudio: + def __init__(self, **kwargs): + self.file_name = kwargs["file_name"] + self.mime_type = kwargs["mime_type"] + self.file_id = "AUDIO" + if kwargs.get("file_size"): + self.file_size = kwargs["file_size"] + else: + self.file_size = 1024 + + +class MockDocument: + def __init__(self, **kwargs): + self.file_name = kwargs["file_name"] + self.mime_type = kwargs["mime_type"] + self.file_id = "DOCUMENT" + if kwargs.get("file_size"): + self.file_size = kwargs["file_size"] + else: + self.file_size = 1024 + + +class MockPhoto: + def __init__(self, **kwargs): + self.date = kwargs["date"] + self.file_unique_id = kwargs["file_unique_id"] + self.file_id = "PHOTO" + if kwargs.get("file_size"): + self.file_size = kwargs["file_size"] + else: + self.file_size = 1024 + + +class MockVoice: + def __init__(self, **kwargs): + self.mime_type = kwargs["mime_type"] + self.date = kwargs["date"] + self.file_id = "VOICE" + if kwargs.get("file_size"): + self.file_size = kwargs["file_size"] + else: + self.file_size = 1024 + + +class MockVideo: + def __init__(self, **kwargs): + self.file_name = kwargs.get("file_name") + self.mime_type = kwargs["mime_type"] + self.file_id = "VIDEO" + if kwargs.get("file_size"): + self.file_size = kwargs["file_size"] + else: + self.file_size = 1024 + + if kwargs.get("width"): + self.width = kwargs["width"] + else: + self.width = 1920 + + if kwargs.get("height"): + self.height = kwargs["height"] + else: + self.height = 1080 + + if kwargs.get("duration"): + self.duration = kwargs["duration"] + else: + self.duration = 1024 + + +class MockVideoNote: + def __init__(self, **kwargs): + self.mime_type = kwargs["mime_type"] + self.file_id = "VIDEO_NOTE" + self.date = kwargs["date"] + + +def platform_generic_path(_path: str) -> str: + platform_specific_path: str = _path + if platform.system() == "Windows": + platform_specific_path = platform_specific_path.replace("/", "\\") + return platform_specific_path + + +def get_file_type(file_id: str): + if file_id == "THUMBNAIL": + return FileType.THUMBNAIL + elif file_id == "CHAT_PHOTO": + return FileType.CHAT_PHOTO + elif file_id == "PHOTO": + return FileType.PHOTO + elif file_id == "VOICE": + return FileType.VOICE + elif file_id == "VIDEO": + return FileType.VIDEO + elif file_id == "DOCUMENT": + return FileType.DOCUMENT + elif file_id == "ENCRYPTED": + return FileType.ENCRYPTED + elif file_id == "TEMP": + return FileType.TEMP + elif file_id == "STICKER": + return FileType.STICKER + elif file_id == "AUDIO": + return FileType.AUDIO + elif file_id == "ANIMATION": + return FileType.ANIMATION + elif file_id == "ENCRYPTED_THUMBNAIL": + return FileType.ENCRYPTED_THUMBNAIL + elif file_id == "WALLPAPER": + return FileType.WALLPAPER + elif file_id == "VIDEO_NOTE": + return FileType.VIDEO_NOTE + elif file_id == "SECURE_RAW": + return FileType.SECURE_RAW + elif file_id == "SECURE": + return FileType.SECURE + elif file_id == "BACKGROUND": + return FileType.BACKGROUND + elif file_id == "DOCUMENT_AS_FILE": + return FileType.DOCUMENT_AS_FILE + + raise ValueError("error file id!") + + +def get_extension(file_id: str, mime_type: str, dot: bool = True): + file_type = get_file_type(file_id=file_id) + guessed_extension = "" + + if file_type in PHOTO_TYPES: + extension = "jpg" + elif file_type == FileType.VOICE: + extension = guessed_extension or "ogg" + elif file_type in (FileType.VIDEO, FileType.ANIMATION, FileType.VIDEO_NOTE): + extension = guessed_extension or "mp4" + elif file_type == FileType.DOCUMENT: + extension = guessed_extension or "zip" + elif file_type == FileType.STICKER: + extension = guessed_extension or "webp" + elif file_type == FileType.AUDIO: + extension = guessed_extension or "mp3" + else: + extension = "unknown" + + if dot: + extension = "." + extension + + return extension diff --git a/tests/test_media_downloader.py b/tests/test_media_downloader.py new file mode 100644 index 0000000..bbdb6d7 --- /dev/null +++ b/tests/test_media_downloader.py @@ -0,0 +1,1096 @@ +"""Unittest module for media downloader.""" +import asyncio +import os +import platform +import queue +import sys +import unittest +from datetime import datetime +from typing import List, Union + +import mock +import pyrogram + +from media_downloader import ( + _can_download, + _check_config, + _get_media_meta, + _is_exist, + app, + download_all_chat, + download_media, + download_task, + main, + save_msg_to_file, + worker, +) +from module.app import Application, DownloadStatus, TaskNode +from module.cloud_drive import CloudDriveConfig +from module.pyrogram_extension import ( + get_extension, + record_download_status, + reset_download_cache, +) + +from .test_common import ( + Chat, + Date, + MockAudio, + MockDocument, + MockMessage, + MockPhoto, + MockVideo, + MockVideoNote, + MockVoice, + get_extension, + platform_generic_path, +) + +MOCK_DIR: str = "/root/project" +if platform.system() == "Windows": + MOCK_DIR = "\\root\\project" +MOCK_CONF = { + "api_id": 123, + "api_hash": "hasw5Tgawsuj67", + "chat": [{"chat_id": 8654123, "last_read_message_id": 0, "ids_to_retry": [1, 2]}], + "media_types": ["audio", "voice", "document", "photo", "video", "video_note"], + "file_formats": {"audio": ["all"], "voice": ["all"], "video": ["all"]}, + "save_path": MOCK_DIR, + "file_name_prefix": ["message_id", "caption", "file_name"], +} + +event_str = "asyncio.AbstractEventLoop.run_forever" +if sys.version_info > (3, 8): + event_str = "asyncio.ProactorEventLoop.run_forever" + + +def os_remove(_: str): + pass + + +def is_exist(file: str): + if os.path.basename(file).find("313 - sucess_exist_down.mp4") != -1: + return True + elif os.path.basename(file).find("422 - exception.mov") != -1: + raise Exception + return False + + +def os_get_file_size(file: str) -> int: + if os.path.basename(file).find("311 - failed_down.mp4") != -1: + return 0 + elif os.path.basename(file).find("312 - sucess_down.mp4") != -1: + return 1024 + elif os.path.basename(file).find("313 - sucess_exist_down.mp4") != -1: + return 1024 + return 0 + + +def rest_app(conf: dict): + config_test = os.path.join(os.path.abspath("."), "config_test.yaml") + data_test = os.path.join(os.path.abspath("."), "data_test.yaml") + if os.path.exists(config_test): + os.remove(config_test) + if os.path.exists(data_test): + os.remove(data_test) + app.total_download_task = 0 + app.is_running = True + app.chat_download_config: dict = {} + # app.already_download_ids_set = set() + app.save_path = os.path.abspath(".") + app.api_id: str = "" + app.api_hash: str = "" + app.media_types: List[str] = [] + app.file_formats: dict = {} + app.proxy: dict = {} + app.restart_program = False + app.config: dict = {} + app.app_data: dict = {} + app.file_path_prefix: List[str] = ["chat_title", "media_datetime"] + app.file_name_prefix: List[str] = ["message_id", "file_name"] + app.file_name_prefix_split: str = " - " + app.log_file_path = os.path.join(os.path.abspath("."), "log") + app.cloud_drive_config = CloudDriveConfig() + app.hide_file_name = False + app.caption_name_dict: dict = {} + app.max_concurrent_transmissions: int = 1 + app.web_host: str = "localhost" + app.web_port: int = 5000 + app.config_file = "config_test.yaml" + app.app_data_file = "data_test.yaml" + app.config = conf + app.assign_config(conf) + app.assign_app_data(conf) + + +def mock_manage_duplicate_file(file_path: str) -> str: + return file_path + + +def raise_keyboard_interrupt(): + raise KeyboardInterrupt + + +async def new_upload_telegram_chat( + client: pyrogram.Client, + upload_user: pyrogram.Client, + app: Application, + node: TaskNode, + message: pyrogram.types.Message, + download_status: DownloadStatus, + file_name: str = None, +): + pass + + +def raise_exception(): + raise Exception + + +def load_config(): + raise ValueError("error load config") + + +class MyQueue: + def __init__(self, queue_list_obj): + self._queue = queue.Queue() + for item in queue_list_obj: + self._queue.put(item) + + async def get(self): + if self._queue.empty(): + raise Exception + return self._queue.get() + + +class MockEventLoop: + def __init__(self): + pass + + def run_until_complete(self, *args, **kwargs): + return {"api_id": 1, "api_hash": "asdf", "ids_to_retry": [1, 2, 3]} + + +class MockAsync: + def __init__(self): + pass + + def get_event_loop(self): + return MockEventLoop() + + +async def async_get_media_meta(chat_id, message, message_media, _type): + result = await _get_media_meta(chat_id, message, message_media, _type) + return result + + +async def async_download_media( + client, message, media_types, file_formats, chat_id=-123 +): + node = TaskNode(chat_id=chat_id) + return await download_media(client, message, media_types, file_formats, node) + + +def mock_move_to_download_path(temp_download_path: str, download_path: str): + pass + + +def mock_check_download_finish(media_size: int, download_path: str, ui_file_name: str): + pass + + +async def new_fetch_message(client: pyrogram.Client, message: pyrogram.types.Message): + return message + + +async def get_chat_history(client, *args, **kwargs): + items = [ + MockMessage( + id=1213, + media=True, + voice=MockVoice( + mime_type="audio/ogg", + date=datetime(2019, 7, 25, 14, 53, 50), + ), + ), + MockMessage( + id=1214, + media=False, + text="test message 1", + ), + MockMessage( + id=1215, + media=False, + text="test message 2", + ), + MockMessage( + id=1216, + media=False, + text="test message 3", + ), + ] + for item in items: + yield item + + +class MockClient: + def __init__(self, *args, **kwargs): + pass + + def __aiter__(self): + return self + + async def start(self): + pass + + async def stop(self): + pass + + async def get_messages(self, *args, **kwargs): + if kwargs["message_ids"] == 7: + return MockMessage( + id=7, + media=True, + chat_id=123456, + chat_title="123456", + date=datetime.now(), + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + elif kwargs["message_ids"] == 8: + return MockMessage( + id=8, + media=True, + chat_id=234567, + chat_title="234567", + date=datetime.now(), + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + elif kwargs["message_ids"] == [1, 2]: + return [ + MockMessage( + id=1, + media=True, + chat_id=234568, + chat_title="234568", + date=datetime.now(), + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ), + MockMessage( + id=2, + media=True, + chat_id=234568, + chat_title="234568", + date=datetime.now(), + video=MockVideo( + file_name="sample_video2.mov", + mime_type="video/mov", + ), + ), + ] + elif kwargs["message_ids"] == 313: + return MockMessage( + id=313, + media=True, + video=MockVideo( + file_name="sucess_exist_down.mp4", + mime_type="video/mp4", + file_size=1024, + ), + ) + elif kwargs["message_ids"] == 312: + return MockMessage( + id=312, + media=True, + video=MockVideo( + file_name="sucess_down.mp4", + mime_type="video/mp4", + file_size=1024, + ), + ) + elif kwargs["message_ids"] == 311: + return MockMessage( + id=311, + media=True, + video=MockVideo( + file_name="failed_down.mp4", + mime_type="video/mp4", + file_size=1024, + ), + ) + return [] + + async def download_media(self, *args, **kwargs): + mock_message = args[0] + if mock_message.id in [7, 8]: + raise pyrogram.errors.exceptions.bad_request_400.BadRequest + elif mock_message.id == 9: + raise pyrogram.errors.exceptions.unauthorized_401.Unauthorized + elif mock_message.id == 11: + raise TypeError + elif mock_message.id == 420: + raise pyrogram.errors.exceptions.flood_420.FloodWait(value=420) + elif mock_message.id == 421: + raise Exception + return kwargs["file_name"] + + async def edit_message_text(self, *args, **kwargs): + return True + + +def check_for_updates(_: dict = None): + pass + + +@mock.patch("media_downloader.get_extension", new=get_extension) +@mock.patch("module.pyrogram_extension.get_extension", new=get_extension) +@mock.patch("media_downloader.fetch_message", new=new_fetch_message) +@mock.patch("media_downloader.get_chat_history_v2", new=get_chat_history) +@mock.patch("media_downloader.RETRY_TIME_OUT", new=0) +@mock.patch("media_downloader.check_for_updates", new=check_for_updates) +class MediaDownloaderTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.loop = asyncio.get_event_loop() + rest_app(MOCK_CONF) + + # @mock.patch("media_downloader.app.save_path", new=MOCK_DIR) + def test_get_media_meta(self): + rest_app(MOCK_CONF) + app.save_path = MOCK_DIR + # Test Voice notes + message = MockMessage( + id=1, + media=True, + chat_title="test1", + date=datetime(2019, 7, 25, 14, 53, 50), + voice=MockVoice( + mime_type="audio/ogg", + date=datetime(2019, 7, 25, 14, 53, 50), + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.voice, "voice") + ) + + self.assertEqual( + ( + platform_generic_path( + "/root/project/test1/2019_07/1 - voice_2019-07-25T14_53_50.ogg" + ), + platform_generic_path( + os.path.join( + app.temp_save_path, "test1/1 - voice_2019-07-25T14_53_50.ogg" + ) + ), + "ogg", + ), + result, + ) + + # Test photos + message = MockMessage( + id=2, + media=True, + date=datetime(2019, 8, 5, 14, 35, 12), + chat_title="test2", + photo=MockPhoto( + date=datetime(2019, 8, 5, 14, 35, 12), file_unique_id="ADAVKJYIFV" + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.photo, "photo") + ) + self.assertEqual( + ( + platform_generic_path("/root/project/test2/2019_08/2 - ADAVKJYIFV.jpg"), + platform_generic_path( + os.path.join(app.temp_save_path, "test2/2 - ADAVKJYIFV.jpg") + ), + None, + ), + result, + ) + + message = MockMessage( + id=2, + media=True, + date=datetime(2019, 8, 5, 14, 35, 12), + chat_title="test2", + media_group_id="AAA213213", + caption="#home #book", + photo=MockPhoto( + date=datetime(2019, 8, 5, 14, 35, 12), file_unique_id="ADAVKJYIFV" + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.photo, "photo") + ) + self.assertEqual( + ( + platform_generic_path( + "/root/project/test2/2019_08/2 - #home #book - ADAVKJYIFV.jpg" + ), + platform_generic_path( + os.path.join( + app.temp_save_path, "test2/2 - #home #book - ADAVKJYIFV.jpg" + ) + ), + None, + ), + result, + ) + + # Test Documents + message = MockMessage( + id=3, + media=True, + chat_title="test2", + document=MockDocument( + file_name="sample_document.pdf", + mime_type="application/pdf", + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.document, "document") + ) + self.assertEqual( + ( + platform_generic_path("/root/project/test2/0/3 - sample_document.pdf"), + platform_generic_path( + os.path.join(app.temp_save_path, "test2/3 - sample_document.pdf") + ), + "pdf", + ), + result, + ) + + before_file_name_prefix_split = app.file_name_prefix_split + app.file_name_prefix_split = "-" + + message = MockMessage( + id=3, + media=True, + chat_title="test2", + media_group_id="BBB213213", + caption="#work", + document=MockDocument( + file_name="sample_document.pdf", + mime_type="application/pdf", + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.document, "document") + ) + self.assertEqual( + ( + platform_generic_path( + "/root/project/test2/0/3-#work-sample_document.pdf" + ), + platform_generic_path( + os.path.join( + app.temp_save_path, "test2/3-#work-sample_document.pdf" + ) + ), + "pdf", + ), + result, + ) + + app.file_name_prefix_split = before_file_name_prefix_split + # Test audio + message = MockMessage( + id=4, + media=True, + date=datetime(2021, 8, 5, 14, 35, 12), + chat_title="test2", + audio=MockAudio( + file_name="sample_audio.mp3", + mime_type="audio/mp3", + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.audio, "audio") + ) + self.assertEqual( + ( + platform_generic_path( + "/root/project/test2/2021_08/4 - sample_audio.mp3" + ), + platform_generic_path( + os.path.join(app.temp_save_path, "test2/4 - sample_audio.mp3") + ), + "mp3", + ), + result, + ) + + # Test Video 1 + message = MockMessage( + id=5, + media=True, + date=datetime(2022, 8, 5, 14, 35, 12), + chat_title="test2", + video=MockVideo( + mime_type="video/mp4", + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.video, "video") + ) + self.assertEqual( + ( + platform_generic_path("/root/project/test2/2022_08/5.mp4"), + platform_generic_path(os.path.join(app.temp_save_path, "test2/5.mp4")), + "mp4", + ), + result, + ) + + # Test Video 2 + message = MockMessage( + id=5, + media=True, + date=datetime(2022, 8, 5, 14, 35, 12), + chat_title="test2", + video=MockVideo( + file_name="test.mp4", + mime_type="video/mp4", + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.video, "video") + ) + self.assertEqual( + ( + platform_generic_path("/root/project/test2/2022_08/5 - test.mp4"), + platform_generic_path( + os.path.join(app.temp_save_path, "test2/5 - test.mp4") + ), + "mp4", + ), + result, + ) + + # Test Video 3: not exist chat_title + message = MockMessage( + id=5, + media=True, + dis_chat=True, + date=datetime(2022, 8, 5, 14, 35, 12), + video=MockVideo( + file_name="test.mp4", + mime_type="video/mp4", + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.video, "video") + ) + + self.assertEqual( + ( + platform_generic_path("/root/project/-123/2022_08/5 - test.mp4"), + platform_generic_path( + os.path.join(app.temp_save_path, "-123/5 - test.mp4") + ), + "mp4", + ), + result, + ) + + # Test VideoNote + message = MockMessage( + id=6, + media=True, + date=datetime(2019, 7, 25, 14, 53, 50), + chat_title="test2", + video_note=MockVideoNote( + mime_type="video/mp4", + date=datetime(2019, 7, 25, 14, 53, 50), + ), + ) + result = self.loop.run_until_complete( + async_get_media_meta(-123, message, message.video_note, "video_note") + ) + self.assertEqual( + ( + platform_generic_path( + "/root/project/test2/2019_07/6 - video_note_2019-07-25T14_53_50.mp4" + ), + platform_generic_path( + os.path.join( + app.temp_save_path, + "test2/6 - video_note_2019-07-25T14_53_50.mp4", + ) + ), + "mp4", + ), + result, + ) + + @mock.patch("media_downloader.app.save_path", new=MOCK_DIR) + @mock.patch("media_downloader.asyncio.sleep", return_value=None) + @mock.patch("media_downloader.logger") + @mock.patch("media_downloader._is_exist", new=is_exist) + @mock.patch( + "media_downloader._move_to_download_path", new=mock_move_to_download_path + ) + @mock.patch( + "media_downloader._check_download_finish", new=mock_check_download_finish + ) + def test_download_media(self, mock_logger, patch_sleep): + reset_download_cache() + rest_app(MOCK_CONF) + client = MockClient() + app.hide_file_name = True + message = MockMessage( + id=5, + media=True, + video=MockVideo( + file_name="sample_video.mp4", + mime_type="video/mp4", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["mp4"]}, -123 + ) + ) + self.assertEqual( + ( + DownloadStatus.SuccessDownload, + platform_generic_path("/root/project/-123/0/5 - sample_video.mp4"), + ), + result, + ) + + message = MockMessage( + id=6, + media=True, + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual( + ( + DownloadStatus.SuccessDownload, + platform_generic_path("/root/project/-123/0/6 - sample_video.mov"), + ), + result, + ) + + # Test re-fetch message success + message = MockMessage( + id=7, + media=True, + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.FailedDownload, None), result) + mock_logger.warning.assert_called_with( + "Message[7]: file reference expired, refetching..." + ) + + # Test re-fetch message failure + message = MockMessage( + id=8, + media=True, + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.FailedDownload, None), result) + mock_logger.error.assert_called_with( + "Message[8]: file reference expired for 3 retries, download skipped." + ) + + # Test other exception + message = MockMessage( + id=9, + media=True, + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.FailedDownload, None), result) + + # Check no media + message = MockMessage( + id=10, + media=None, + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.SkipDownload, None), result) + + # Test timeout + message = MockMessage( + id=11, + media=True, + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.FailedDownload, None), result) + mock_logger.error.assert_called_with( + "Message[11]: Timing out after 3 reties, download skipped." + ) + + # Test file name with out suffix + message = MockMessage( + id=12, + media=True, + video=MockVideo( + file_name="sample_video", + mime_type="video/mp4", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual( + ( + DownloadStatus.SuccessDownload, + platform_generic_path("/root/project/-123/0/12 - sample_video.mp4"), + ), + result, + ) + + # Test FloodWait 420 + message = MockMessage( + id=420, + media=True, + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.FailedDownload, None), result) + mock_logger.warning.assert_called_with("Message[{}]: FlowWait {}", 420, 420) + + # Test other Exception + message = MockMessage( + id=421, + media=True, + video=MockVideo( + file_name="sample_video.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.FailedDownload, None), result) + + # Test other Exception + message = MockMessage( + id=422, + media=True, + video=MockVideo( + file_name="422 - exception.mov", + mime_type="video/mov", + ), + ) + result = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["all"]} + ) + ) + self.assertEqual((DownloadStatus.FailedDownload, None), result) + + @mock.patch("media_downloader.HookClient", new=MockClient) + @mock.patch("media_downloader.asyncio.Queue.put") + def test_download_task(self, moc_put): + rest_app(MOCK_CONF) + client = MockClient() + app.chat_download_config[8654123].download_filter = "id != 1213" + self.loop.run_until_complete(download_all_chat(client)) + moc_put.assert_called() + + def test_can_download(self): + file_formats = { + "audio": ["mp3"], + "video": ["mp4"], + "document": ["all"], + } + result = _can_download("audio", file_formats, "mp3") + self.assertEqual(result, True) + + result1 = _can_download("audio", file_formats, "ogg") + self.assertEqual(result1, False) + + result2 = _can_download("document", file_formats, "pdf") + self.assertEqual(result2, True) + + result3 = _can_download("document", file_formats, "epub") + self.assertEqual(result3, True) + + def test_is_exist(self): + this_dir = os.path.dirname(os.path.abspath(__file__)) + result = _is_exist(os.path.join(this_dir, "__init__.py")) + self.assertEqual(result, True) + + result1 = _is_exist(os.path.join(this_dir, "init.py")) + self.assertEqual(result1, False) + + result2 = _is_exist(this_dir) + self.assertEqual(result2, False) + + @mock.patch("media_downloader.os.makedirs") + @mock.patch("builtins.open", new_callable=mock.mock_open) + def test_save_msg_to_file(self, mock_open, mock_makedirs): + rest_app(MOCK_CONF) + app.enable_download_txt = True + app.temp_save_path = "/tmp" + app.date_format = "%Y_%m" + + message = MockMessage( + id=123, + dis_chat=True, + chat=Chat(chat_id=456, chat_title="Test Chat"), + date=datetime(2023, 5, 15, 10, 30, 0), + text="This is a test message", + ) + + expected_file_path = platform_generic_path( + "/root/project/Test Chat/2023_05/123.txt" + ) + + result = self.loop.run_until_complete(save_msg_to_file(app, 456, message)) + + self.assertEqual(result, (DownloadStatus.SuccessDownload, expected_file_path)) + mock_makedirs.assert_called_once_with( + os.path.dirname(expected_file_path), exist_ok=True + ) + mock_open.assert_called_once_with(expected_file_path, "w", encoding="utf-8") + mock_open().write.assert_called_once_with("This is a test message") + + @mock.patch("media_downloader.RETRY_TIME_OUT", new=0) + @mock.patch("media_downloader.os.path.getsize", new=os_get_file_size) + @mock.patch("media_downloader.os.remove", new=os_remove) + @mock.patch("media_downloader._is_exist", new=is_exist) + @mock.patch( + "media_downloader._move_to_download_path", new=mock_move_to_download_path + ) + def test_issues_311(self): + # see https://github.com/Dineshkarthik/telegram_media_downloader/issues/311 + rest_app(MOCK_CONF) + + client = MockClient() + # 1. test `TimeOutError` + message = MockMessage( + id=311, + media=True, + video=MockVideo( + file_name="failed_down.mp4", + mime_type="video/mp4", + file_size=1024, + ), + ) + + media_size = getattr(message.video, "file_size") + self.assertEqual(media_size, 1024) + + res = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["mp4"]} + ) + ) + self.assertEqual(res, (DownloadStatus.FailedDownload, None)) + + # 2. test sucess download + rest_app(MOCK_CONF) + message = MockMessage( + id=312, + media=True, + video=MockVideo( + file_name="sucess_down.mp4", + mime_type="video/mp4", + file_size=1024, + ), + ) + + res = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["mp4"]} + ) + ) + + self.assertEqual( + res, + ( + DownloadStatus.SuccessDownload, + platform_generic_path("/root/project/-123/0/312 - sucess_down.mp4"), + ), + ) + + rest_app(MOCK_CONF) + # 3. test already download + message = MockMessage( + id=313, + media=True, + video=MockVideo( + file_name="sucess_exist_down.mp4", + mime_type="video/mp4", + file_size=1024, + ), + ) + + res = self.loop.run_until_complete( + async_download_media( + client, message, ["video", "photo"], {"video": ["mp4"]} + ) + ) + + self.assertEqual(res, (DownloadStatus.SkipDownload, None)) + + @mock.patch("media_downloader.HookClient", new=MockClient) + @mock.patch("media_downloader.RETRY_TIME_OUT", new=1) + @mock.patch("media_downloader.logger") + def test_main_with_bot(self, mock_logger): + rest_app(MOCK_CONF) + + main() + + mock_logger.success.assert_called_with( + "Updated last read message_id to config file,total download 0, total upload file 0" + ) + + @mock.patch("media_downloader.app.pre_run", new=raise_keyboard_interrupt) + @mock.patch("media_downloader.HookClient", new=MockClient) + @mock.patch("media_downloader.RETRY_TIME_OUT", new=1) + @mock.patch("media_downloader.logger") + def test_keyboard_interrupt(self, mock_logger): + rest_app(MOCK_CONF) + + main() + + mock_logger.info.assert_any_call("KeyboardInterrupt") + mock_logger.success.assert_called_with( + "Updated last read message_id to config file,total download 0, total upload file 0" + ) + + @mock.patch("media_downloader.app.pre_run", new=raise_exception) + @mock.patch("media_downloader.HookClient", new=MockClient) + @mock.patch("media_downloader.RETRY_TIME_OUT", new=1) + @mock.patch("media_downloader.logger") + def test_other_exception(self, mock_logger): + rest_app(MOCK_CONF) + + main() + + mock_logger.success.assert_called_with( + "Updated last read message_id to config file,total download 0, total upload file 0" + ) + + @mock.patch("media_downloader._load_config", new=load_config) + @mock.patch("media_downloader.logger") + def test_check_config(self, mock_logger): + _check_config() + mock_logger.exception.assert_called_with("load config error: error load config") + + def test_check_config_suc(self): + app.update_config() + self.assertEqual(_check_config(), True) + + # @mock.patch( + # "media_downloader.queue", + # new=MyQueue( + # [ + # ( + # MockMessage( + # id=312, + # media=True, + # chat_id=8654123, + # chat_title="8654123", + # video=MockVideo( + # file_name="sucess_down.mp4", + # mime_type="video/mp4", + # file_size=1024, + # ), + # ), + # TaskNode(chat_id=8654123, upload_telegram_chat_id=123456), + # ), + # ( + # MockMessage( + # id=333, + # media=True, + # chat_id=8654123, + # chat_title="8654123", + # text="123", + # ), + # TaskNode(chat_id=8654123, upload_telegram_chat_id=123456), + # ), + # ] + # ), + # ) + # @mock.patch("media_downloader.app.set_download_id", new=new_set_download_id) + # @mock.patch("media_downloader.upload_telegram_chat", new=new_upload_telegram_chat) + # @mock.patch("media_downloader.os.remove") + # @mock.patch( + # "media_downloader._move_to_download_path", new=mock_move_to_download_path + # ) + # @mock.patch("media_downloader.os.path.getsize", new=os_get_file_size) + # def test_upload_telegram_chat(self, mock_remove): + # rest_app(MOCK_CONF) + # client = MockClient() + # app.chat_download_config[8654123].last_read_message_id = 0 + # self.loop.run_until_complete(worker(client)) + # mock_remove.assert_called_with( + # platform_generic_path("/root/project/8654123/0/312 - sucess_down.mp4") + # ) + + @classmethod + def tearDownClass(cls): + cls.loop.close() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/test_cypto.py b/tests/utils/test_cypto.py new file mode 100644 index 0000000..4b87099 --- /dev/null +++ b/tests/utils/test_cypto.py @@ -0,0 +1,55 @@ +"""Test Crypto""" + +import sys +import unittest + +from utils.crypto import AesBase64 + +sys.path.append("..") # Adds higher directory to python modules path. + + +class TestAesBase64(unittest.TestCase): + def test_aes_base64_encryption(self): + key = "This is a key123" + iv = "This is an iv456" + aes = AesBase64(key, iv) + content = "Some text for encryption." + encrypted_content = aes.encrypt(content) + decrypted_content = aes.decrypt(encrypted_content) + self.assertEqual(content, decrypted_content) + + def test_aes_base64_encryption_with_special_characters(self): + key = "Special!@#$%^&*(" + iv = "Characters123456" + aes = AesBase64(key, iv) + content = "Text with special characters!@#$%^&*()_+-=[]{}|;" + encrypted_content = aes.encrypt(content) + decrypted_content = aes.decrypt(encrypted_content) + self.assertEqual(content, decrypted_content) + + def test_aes_base64_encryption_with_empty_string(self): + key = "Empty string tes" + iv = "1234567890123456" + aes = AesBase64(key, iv) + content = "" + encrypted_content = aes.encrypt(content) + decrypted_content = aes.decrypt(encrypted_content) + self.assertEqual(content, decrypted_content) + + def test_aes_base64_pkcs7padding(self): + key = "Test padding key" + iv = "1234567890123456" + aes = AesBase64(key, iv) + content = "Test padding." + padded_content = aes.pkcs7padding(content) + self.assertEqual(len(padded_content) % 16, 0) + self.assertEqual(padded_content[-1], chr(16 - len(content) % 16)) + + def test_aes_base64_pkcs7unpadding(self): + key = "Test unpadding key" + iv = "1234567890123456" + aes = AesBase64(key, iv) + content = "Test unpadding." + padded_content = aes.pkcs7padding(content) + unpadded_content = aes.pkcs7unpadding(padded_content) + self.assertEqual(unpadded_content, content) diff --git a/tests/utils/test_file_management.py b/tests/utils/test_file_management.py new file mode 100644 index 0000000..bf4fc4d --- /dev/null +++ b/tests/utils/test_file_management.py @@ -0,0 +1,40 @@ +"""Unittest module for media downloader.""" +import os +import sys +import tempfile +import unittest +from pathlib import Path + +import mock + +sys.path.append("..") # Adds higher directory to python modules path. +from utils.file_management import get_next_name, manage_duplicate_file + + +class FileManagementTestCase(unittest.TestCase): + def setUp(self): + self.this_dir = os.path.dirname(os.path.abspath(__file__)) + self.test_file = os.path.join(self.this_dir, "file-test.txt") + self.test_file_copy_1 = os.path.join(self.this_dir, "file-test-copy1.txt") + self.test_file_copy_2 = os.path.join(self.this_dir, "file-test-copy2.txt") + f = open(self.test_file, "w+") + f.write("dummy file") + f.close() + Path(self.test_file_copy_1).touch() + Path(self.test_file_copy_2).touch() + + def test_get_next_name(self): + result = get_next_name(self.test_file) + excepted_result = os.path.join(self.this_dir, "file-test-copy3.txt") + self.assertEqual(result, excepted_result) + + def test_manage_duplicate_file(self): + result = manage_duplicate_file(self.test_file_copy_2) + self.assertEqual(result, self.test_file_copy_1) + + result1 = manage_duplicate_file(self.test_file_copy_1) + self.assertEqual(result1, self.test_file_copy_1) + + def tearDown(self): + os.remove(self.test_file) + os.remove(self.test_file_copy_1) diff --git a/tests/utils/test_filter.py b/tests/utils/test_filter.py new file mode 100644 index 0000000..e2595b1 --- /dev/null +++ b/tests/utils/test_filter.py @@ -0,0 +1,450 @@ +"""Unittest module for media downloader.""" +import sys +import unittest +from datetime import datetime + +import mock + +from module.filter import Filter, MetaData +from module.pyrogram_extension import set_meta_data +from tests.test_common import ( + Chat, + Date, + MockAudio, + MockDocument, + MockMessage, + MockPhoto, + MockUser, + MockVideo, + MockVideoNote, + MockVoice, + get_extension, +) +from utils.format import replace_date_time + +sys.path.append("..") # Adds higher directory to python modules path. + + +def filter_exec(download_filter: Filter, filter_str: str) -> bool: + filter_str = replace_date_time(filter_str) + return download_filter.exec(filter_str) + + +def check_filter_exec(download_filter: Filter, filter_str: str) -> bool: + filter_str = replace_date_time(filter_str) + return download_filter.check_filter(filter_str) + + +@mock.patch("module.pyrogram_extension.get_extension", new=get_extension) +class FilterTestCase(unittest.TestCase): + def test_string_filter(self): + download_filter = Filter() + self.assertRaises(ValueError, filter_exec, download_filter, "213") + + meta = MetaData() + + message = MockMessage( + id=5, + media=True, + date=datetime(2022, 8, 5, 14, 35, 12), + chat_title="test2", + caption=None, + video=MockVideo( + mime_type="video/mp4", + file_size=1024 * 1024 * 10, + file_name="test.mp4", + width=1920, + height=1080, + duration=35, + ), + from_user=MockUser( + username="coco", + id=123, + ), + ) + + set_meta_data(meta, message) + + self.assertEqual(meta.message_id, 5) + self.assertEqual(meta.message_date, datetime(2022, 8, 5, 14, 35, 12)) + self.assertEqual(meta.message_caption, "") + self.assertEqual(meta.media_file_size, 1024 * 1024 * 10) + self.assertEqual(meta.media_width, 1920) + self.assertEqual(meta.media_height, 1080) + self.assertEqual(meta.media_file_name, "test.mp4") + self.assertEqual(meta.media_duration, 35) + self.assertEqual(meta.media_type, "video") + self.assertEqual(meta.file_extension, "mp4") + + download_filter.set_meta_data(meta) + + self.assertEqual(filter_exec(download_filter, "media_file_size == 1"), False) + self.assertEqual(filter_exec(download_filter, "media_file_size > 1024"), True) + + # str + self.assertEqual( + filter_exec(download_filter, "media_file_name == 'test.mp4'"), True + ) + self.assertEqual( + filter_exec(download_filter, "media_file_name == 'test2.mp4'"), False + ) + # re str + self.assertEqual( + filter_exec(download_filter, "media_file_name == r'test.*mp4'"), True + ) + + self.assertEqual( + filter_exec(download_filter, "media_file_name == r'test\.*mp4'"), True + ) + + self.assertEqual( + filter_exec(download_filter, "media_file_name == r'test2.*mp4'"), False + ) + + self.assertEqual( + filter_exec(download_filter, "media_file_name != r'test2.*mp4'"), True + ) + self.assertEqual( + filter_exec(download_filter, "media_file_name != r'test2.*mp4'"), True + ) + + # int + self.assertEqual(filter_exec(download_filter, "media_duration > 60"), False) + self.assertEqual(filter_exec(download_filter, "media_duration <= 60"), True) + self.assertEqual( + filter_exec( + download_filter, "media_width >= 1920 and media_height >= 1080" + ), + True, + ) + self.assertEqual( + filter_exec(download_filter, "media_width >= 2560 && media_height >= 1440"), + False, + ) + self.assertEqual( + filter_exec( + download_filter, + "media_width >= 2560 && media_height >= 1440 or media_file_name == 'test.mp4'", + ), + True, + ) + + # datetime + # 2020.03 + self.assertEqual( + filter_exec( + download_filter, "message_date >= 2022.03 and message_date <= 2022.08" + ), + False, + ) + self.assertEqual( + filter_exec( + download_filter, "message_date >= 2022.03 and message_date <= 2022.09" + ), + True, + ) + + # 2020.03.04 + self.assertEqual( + filter_exec( + download_filter, + "message_date >= 2022.03.04 and message_date <= 2022.03.08", + ), + False, + ) + self.assertEqual( + filter_exec( + download_filter, + "message_date >= 2022.03.04 and message_date <= 2022.08.06", + ), + True, + ) + + # 2020.03.04 14:50 + self.assertEqual( + filter_exec( + download_filter, + "message_date >= 2022.03.04 14:50 and message_date <= 2022.03.08", + ), + False, + ) + self.assertEqual( + filter_exec( + download_filter, + "message_date >= 2022.03.04 and message_date <= 2022.08.05 14:36", + ), + True, + ) + + # 2020.03.04 14:50:15 + self.assertEqual( + filter_exec( + download_filter, + "message_date >= 2022.03.04 14:50:15 and message_date <= 2022.03.08", + ), + False, + ) + self.assertEqual( + filter_exec( + download_filter, + "message_date >= 2022.03.04 14:50:15 and message_date <= 2022.08.05 14:35:12", + ), + True, + ) + + # test not exist value + self.assertRaises( + ValueError, + filter_exec, + download_filter, + "message_date >= 2022.03.04 && message_date <= 2022.08.06 && not_exist == True", + ) + + download_filter.set_debug(True) + + # test file_size + self.assertEqual(filter_exec(download_filter, "file_size >= 10MB"), True) + + self.assertEqual(filter_exec(download_filter, "file_size >= 11MB"), False) + + self.assertEqual(filter_exec(download_filter, "file_size >= 11GB"), False) + + self.assertEqual(filter_exec(download_filter, "file_size <= 11GB"), True) + + self.assertEqual( + filter_exec(download_filter, "1024 * 1024 * 1024 * 11 == 11GB"), True + ) + + # test caption + self.assertEqual(filter_exec(download_filter, "caption == r'.*#test.*'"), False) + + # test media_type + self.assertEqual(filter_exec(download_filter, "media_type == 'video'"), True) + + self.assertEqual(filter_exec(download_filter, "media_type == 'audio'"), False) + + self.assertEqual( + filter_exec(download_filter, "media_type == r'(video|audio)'"), True + ) + + self.assertEqual( + filter_exec(download_filter, "media_type != r'(video|audio)'"), False + ) + + # test file_extension + self.assertEqual(filter_exec(download_filter, "file_extension == 'mp4'"), True) + + self.assertEqual(filter_exec(download_filter, "file_extension == 'mp3'"), False) + + self.assertEqual( + filter_exec(download_filter, "file_extension == r'(mp4|mp3)'"), True + ) + + self.assertEqual( + filter_exec(download_filter, "file_extension != r'(mp4|mp3)'"), False + ) + + # test sender + self.assertEqual(filter_exec(download_filter, "sender_name == 'coco'"), True) + + self.assertEqual(filter_exec(download_filter, "sender_id == 1"), False) + + def test_null_obj(self): + download_filter = Filter() + + meta = MetaData() + + message = MockMessage( + id=5, + media=True, + date=datetime(2022, 8, 5, 14, 35, 12), + chat_title="test2", + caption=None, + video=MockPhoto( + file_size=1024 * 1024 * 10, + date=datetime(2019, 8, 5, 14, 35, 12), + file_unique_id="ADAVKJYIFV", + ), + ) + + set_meta_data(meta, message) + + download_filter.set_meta_data(meta) + + # test media_duration + self.assertEqual(filter_exec(download_filter, "media_duration < 1"), False) + self.assertEqual(filter_exec(download_filter, "media_duration <= 1"), False) + self.assertEqual(filter_exec(download_filter, "media_duration != 1"), False) + self.assertEqual(filter_exec(download_filter, "media_duration == 1"), False) + + def test_str_obj(self): + download_filter = Filter() + self.assertRaises(ValueError, filter_exec, download_filter, "213") + + meta = MetaData() + + message = MockMessage( + id=5, + media=True, + date=datetime(2022, 8, 5, 14, 35, 12), + chat_title="test2", + caption="#中文最吊 #哈啰", + video=MockVideo( + mime_type="video/mp4", + file_size=1024 * 1024 * 10, + file_name="test.mp4", + width=1920, + height=1080, + duration=35, + ), + reply_to_message_id=4, + ) + + set_meta_data(meta, message) + + self.assertEqual(meta.message_id, 5) + self.assertEqual(meta.message_date, datetime(2022, 8, 5, 14, 35, 12)) + self.assertEqual(meta.message_caption, "#中文最吊 #哈啰") + self.assertEqual(meta.media_file_size, 1024 * 1024 * 10) + self.assertEqual(meta.media_width, 1920) + self.assertEqual(meta.media_height, 1080) + self.assertEqual(meta.media_file_name, "test.mp4") + self.assertEqual(meta.media_duration, 35) + + download_filter.set_meta_data(meta) + download_filter.set_debug(True) + + # test caption + self.assertEqual(filter_exec(download_filter, "caption == r'.*#test.*'"), False) + + self.assertEqual(filter_exec(download_filter, "caption == r'.*#中文.*'"), True) + + self.assertEqual(filter_exec(download_filter, "caption == r'.*#中文啊.*'"), False) + + self.assertEqual(filter_exec(download_filter, "reply_to_message_id == 4"), True) + self.assertEqual( + filter_exec(download_filter, "reply_to_message_id != 4"), False + ) + self.assertEqual(filter_exec(download_filter, "reply_to_message_id >= 4"), True) + + def test_check_filter(self): + download_filter = Filter() + meta = MetaData() + + message = MockMessage( + id=5, + media=True, + date=datetime(2022, 8, 5, 14, 35, 12), + chat_title="test2", + caption=None, + video=MockVideo( + mime_type="video/mp4", + file_size=1024 * 1024 * 10, + file_name="test.mp4", + width=1920, + height=1080, + duration=35, + ), + ) + + set_meta_data(meta, message) + + download_filter.set_debug(True) + download_filter.set_meta_data(meta) + + # 1. == + # 1.1 restring + self.assertEqual( + check_filter_exec(download_filter, "caption == rr'.*#中文啊.*'"), + (False, "Syntax error at '.*#中文啊.*'"), + ) + self.assertEqual( + check_filter_exec(download_filter, "caption == r'.*#中文啊.*'"), (True, None) + ) + self.assertEqual( + check_filter_exec(download_filter, "caption tis r'.*#中文啊.*'"), + (False, "Syntax error at 'tis'"), + ) + # 1.2 string + self.assertEqual( + check_filter_exec(download_filter, "caption = '.*#中文啊.*'"), (True, None) + ) + + # 2. check type + # 2.1 str + self.assertEqual( + check_filter_exec(download_filter, "caption = 1"), + (False, "caption is str but 1 is not"), + ) + self.assertEqual( + check_filter_exec(download_filter, "caption = 3KB"), + (False, "caption is str but 3072 is not"), + ) + # 2.2 datetime + self.assertEqual( + check_filter_exec(download_filter, "message_date == '.*#中文啊.*'"), + (False, "2022-08-05 14:35:12 is datetime but .*#中文啊.* is not"), + ) + + # 2.3 int + self.assertEqual( + check_filter_exec(download_filter, "id == '.*'"), + (False, "5 is int but .* is not"), + ) + self.assertEqual( + check_filter_exec(download_filter, "id == .*"), + (False, "Syntax error at '*'"), + ) + self.assertEqual( + check_filter_exec(download_filter, "id == ."), + (False, "Syntax error at EOF"), + ) + self.assertEqual( + check_filter_exec(download_filter, "id == ."), + (False, "Syntax error at EOF"), + ) + # 2.3.1 custom token + self.assertEqual( + check_filter_exec(download_filter, "file_size == 3KB"), (True, None) + ) + self.assertEqual( + check_filter_exec(download_filter, "file_size == 3kb"), + (False, "Syntax error at 'kb'"), + ) + + # 3. error name + self.assertEqual( + check_filter_exec(download_filter, "caption2 == .*#中文啊.*'"), + (False, "Undefined name caption2"), + ) + + # 4. datetime + self.assertEqual( + check_filter_exec(download_filter, "message_date == 2023/0b-05"), + (False, "Syntax error at 'b'"), + ) + self.assertEqual( + check_filter_exec(download_filter, "message_date == 2023/01/45")[0], False + ) + + def test_normal(self): + download_filter = Filter() + print(download_filter.filter.names) + meta = MetaData(datetime(2022, 3, 8, 10, 0, 0), 0, "#高桥千x", 0, 0, 0, "", 0) + download_filter.set_meta_data(meta) + self.assertEqual(check_filter_exec(download_filter, "id > 1"), (True, None)) + download_filter.set_debug(True) + filter_exec(download_filter, "caption == r'.*高桥.*'") + + download_filter2 = Filter() + meta2 = MetaData(datetime(2022, 3, 8, 10, 0, 0), 0, "", 0, 0, 0, "", 0) + download_filter2.set_meta_data(meta2) + download_filter2.set_debug(True) + filter_exec(download_filter2, "caption == r'.*高桥.*'") + print(download_filter.filter.names) + + download_filter.set_meta_data(meta) + self.assertEqual(check_filter_exec(download_filter, "id > 1"), (True, None)) + download_filter.set_debug(True) + filter_exec(download_filter, "caption == r'.*高桥.*'") + filter_exec(download_filter, "caption == r'.*高桥.*'") diff --git a/tests/utils/test_format.py b/tests/utils/test_format.py new file mode 100644 index 0000000..80df66c --- /dev/null +++ b/tests/utils/test_format.py @@ -0,0 +1,255 @@ +"""Unittest module for media downloader.""" +import os +import sys +import unittest +from dataclasses import asdict +from unittest.mock import patch + +from utils.format import ( + Link, + create_progress_bar, + extract_info_from_link, + format_byte, + get_byte_from_str, + replace_date_time, + truncate_filename, + validate_title, +) + +sys.path.append("..") # Adds higher directory to python modules path. + + +class FormatTestCase(unittest.TestCase): + def test_format_byte(self): + byte_list = [ + "KB", + "MB", + "GB", + "TB", + "PB", + "EB", + "ZB", + "YB", + "BB", + "NB", + "DB", + "CB", + ] + + self.assertEqual(format_byte(0.1), "0.8b") + self.assertEqual(format_byte(1), "1B") + + for i, value in enumerate(byte_list): + self.assertEqual(format_byte(pow(1024, i + 1)), "1.0" + value) + + try: + format_byte(-1) + except Exception as e: + self.assertEqual(isinstance(e, ValueError), True) + + def test_replace_date_time(self): + self.assertEqual( + replace_date_time(""), + "", + ) + + # split by '.' + self.assertEqual( + replace_date_time("xxxxx 2020.03.08 xxxxxxxxx"), + "xxxxx 2020-03-08 00:00:00 xxxxxxxxx", + ) + + # split by '-' + self.assertEqual( + replace_date_time("xxxxx 2020-03-08 xxxxxxxxxxxx"), + "xxxxx 2020-03-08 00:00:00 xxxxxxxxxxxx", + ) + + # split by '/' + self.assertEqual( + replace_date_time("xasd as 2020/03/08 21321fszv"), + "xasd as 2020-03-08 00:00:00 21321fszv", + ) + + # more different date + self.assertEqual( + replace_date_time("xxxxx 2020.03.08 2020.03.09 14:51 xxxxxxxxx"), + "xxxxx 2020-03-08 00:00:00 2020-03-09 14:51:00 xxxxxxxxx", + ) + + # more space + self.assertEqual( + replace_date_time("xxxxx 2020.03.08 2020.03.09 14:51 xxxxxxxxx"), + "xxxxx 2020-03-08 00:00:00 2020-03-09 14:51:00 xxxxxxxxx", + ) + + # more date format + self.assertEqual( + replace_date_time("xasd as 2020/03 21321fszv"), + "xasd as 2020-03-01 00:00:00 21321fszv", + ) + self.assertEqual( + replace_date_time("xasd as 2020-03 21321fszv"), + "xasd as 2020-03-01 00:00:00 21321fszv", + ) + self.assertEqual( + replace_date_time("xasd as 2020.03 21321fszv"), + "xasd as 2020-03-01 00:00:00 21321fszv", + ) + + def test_get_byte_from_str(self): + # B + self.assertEqual(get_byte_from_str("2B"), 2) + # KB + self.assertEqual(get_byte_from_str("2KB"), 2 * 1024) + self.assertEqual(get_byte_from_str("1024KB"), 1024 * 1024) + self.assertEqual(get_byte_from_str("2024KB"), 2024 * 1024) + self.assertEqual(get_byte_from_str("4000KB"), 4000 * 1024) + + # MB + self.assertEqual(get_byte_from_str("2MB"), 2 * 1024 * 1024) + self.assertEqual(get_byte_from_str("1024MB"), 1024 * 1024 * 1024) + + # GB + self.assertEqual(get_byte_from_str("2GB"), 2 * 1024 * 1024 * 1024) + + # TB + self.assertEqual(get_byte_from_str("2TB"), 2 * 1024 * 1024 * 1024 * 1024) + self.assertEqual(get_byte_from_str("1024TB"), 1024 * 1024 * 1024 * 1024 * 1024) + + # more str + self.assertEqual(get_byte_from_str("2BW"), 2) + self.assertEqual(get_byte_from_str("2WBW"), None) + + self.assertEqual(get_byte_from_str("2CB"), None) + + def test_extract_info_from_link(self): + test_cases = [ + ("https://t.me/", Link(group_id=None)), + ("https://t.me/username/1234", Link(group_id="username", post_id=1234)), + ("https://t.me/username", Link(group_id="username")), + ("https://t.me/c/213213/91011", Link(group_id=-100213213, post_id=91011)), + ( + "https://t.me/test123/1/1234", + Link(group_id="test123", topic_id=1, post_id=1234), + ), + ("me", Link(group_id="me")), + ("self", Link(group_id="self")), + ( + "https://t.me/opencfdchannel/4434?comment=360409", + Link(group_id="opencfdchannel", comment_id=360409), + ), + ("https://t.me/telegram/193", Link(group_id="telegram", post_id=193)), + ( + "https://t.me/c/1697797156/151", + Link(group_id=-1001697797156, post_id=151), + ), + ( + "https://t.me/iFreeKnow/45662/55005", + Link(group_id="iFreeKnow", topic_id=45662, post_id=55005), + ), + ( + "https://t.me/c/1492447836/251015/251021", + Link(group_id=-1001492447836, topic_id=251015, post_id=251021), + ), + ] + + for link, expected in test_cases: + result = extract_info_from_link(link) + self.assertEqual(asdict(result), asdict(expected)) + + def test_create_progress_bar(self): + progress = 50 + progress_bar = create_progress_bar(progress) + self.assertEqual(progress_bar, "█████░░░░░") + + def test_create_progress_bar_with_custom_bars(self): + progress = 75 + total_bars = 20 + progress_bar = create_progress_bar(progress, total_bars) + self.assertEqual(progress_bar, "███████████████░░░░░") + + +class TestTruncateFilename(unittest.TestCase): + def test_truncate_filename(self): + test_cases = [ + ("testfile.txt", 240, "testfile.txt"), + ("testfile.txt", 5, "t.txt"), + ("a" * 240 + ".txt", 240, "a" * 236 + ".txt"), + ("a" * 241 + ".txt", 240, "a" * 236 + ".txt"), + ] + + for path, limit, expected in test_cases: + self.assertEqual(truncate_filename(path, limit), expected) + + @unittest.skipIf(sys.platform.startswith("win"), "requires Unix-based system") + def test_linux_filename_too_long(self): + long_filename = "a" * 265 + ".txt" + with self.assertRaises(OSError): + with open(long_filename, "w") as f: + f.write("test") + + long_filename = "a" * 265 + ".txt" + long_filename = truncate_filename(long_filename) + ".temp" + try: + with open(long_filename, "w") as f: + f.write("test") + os.remove(long_filename) + except Exception: + self.assertEqual(False, True) + + @unittest.skipIf(not sys.platform.startswith("win"), "requires Windows system") + def test_windows_filename_too_long(self): + long_filename = "a" * 265 + ".txt" + with self.assertRaises(OSError): + with open(long_filename, "w") as f: + f.write("test") + + long_filename = "a" * 265 + ".txt" + long_filename = truncate_filename(long_filename) + ".temp" + try: + with open(long_filename, "w") as f: + f.write("test") + os.remove(long_filename) + except Exception: + self.assertEqual(False, True) + + @patch("builtins.open", unittest.mock.mock_open()) + def test_file_creation(self): + file_name = "a" * 240 + ".txt" + truncated_file_name = truncate_filename(file_name) + + with open(truncated_file_name, "w") as f: + f.write("test") + + open.assert_called_once_with(truncated_file_name, "w") + + +class TestValidateTitle(unittest.TestCase): + def test_validate_title(self): + test_cases = [ + ("Hello, World!", "Hello, World!"), + ("Invalid/Title", "Invalid_Title"), + ("File\\Name", "File_Name"), + ("Colons:Are:Not:Allowed", "Colons_Are_Not_Allowed"), + ("Asterisks*In*Title", "Asterisks_In_Title"), + ("Question?Mark", "Question_Mark"), + ('Double"Quotes', "Double_Quotes"), + ("LessThan", "Greater_Than"), + ("Pipe|Symbol", "Pipe_Symbol"), + ("Multi\nLine", "Multi_Line"), + ] + + for title, expected in test_cases: + with self.subTest(title=title, expected=expected): + self.assertEqual(validate_title(title), expected) + + @patch("utils.format.re.sub") + def test_mock_re_sub(self, mock_re_sub): + title = "Invalid/Title" + mock_re_sub.return_value = "Mocked_Title" + + result = validate_title(title) + self.assertEqual(result, "Mocked_Title") + mock_re_sub.assert_called_once_with(r"[/\\:*?\"<>|\n]", "_", title) diff --git a/tests/utils/test_log.py b/tests/utils/test_log.py new file mode 100644 index 0000000..71cf7a1 --- /dev/null +++ b/tests/utils/test_log.py @@ -0,0 +1,30 @@ +"""Unittest module for log handlers.""" +import os +import sys +import unittest + +import mock + +sys.path.append("..") # Adds higher directory to python modules path. +from utils.log import LogFilter + + +class MockLog: + """ + Mock logs. + """ + + def __init__(self, **kwargs): + self.funcName = kwargs["funcName"] + + +class MetaTestCase(unittest.TestCase): + def test_log_filter(self): + result = LogFilter().filter(MockLog(funcName="invoke")) + self.assertEqual(result, False) + + result1 = LogFilter().filter(MockLog(funcName="get_file")) + self.assertEqual(result1, True) + + result2 = LogFilter().filter(MockLog(funcName="Synced")) + self.assertEqual(result2, True) diff --git a/tests/utils/test_meta.py b/tests/utils/test_meta.py new file mode 100644 index 0000000..a54c275 --- /dev/null +++ b/tests/utils/test_meta.py @@ -0,0 +1,23 @@ +"""Unittest module for media downloader.""" +import os +import sys +import unittest + +import mock + +sys.path.append("..") # Adds higher directory to python modules path. +from utils.meta import print_meta + + +class MetaTestCase(unittest.TestCase): + @mock.patch("utils.meta.APP_VERSION", "test-version 1.0.0") + @mock.patch("utils.meta.DEVICE_MODEL", "CPython X.X.X") + @mock.patch("utils.meta.SYSTEM_VERSION", "System xx.x.xx") + @mock.patch("media_downloader.logger") + def test_print_meta(self, mock_logger): + print_meta(mock_logger) + calls = [ + mock.call.info("Device: CPython X.X.X - test-version 1.0.0"), + mock.call.info("System: System xx.x.xx (EN)"), + ] + mock_logger.assert_has_calls(calls, any_order=True) diff --git a/tests/utils/test_updates.py b/tests/utils/test_updates.py new file mode 100644 index 0000000..2617e53 --- /dev/null +++ b/tests/utils/test_updates.py @@ -0,0 +1,147 @@ +"""Unittest module for update checker.""" +import json +import sys +import unittest + +import mock + +sys.path.append("..") # Adds higher directory to python modules path. +from utils.updates import check_for_updates, get_latest_release + + +class FakeHTTPSConnection: + def __init__(self, status): + self.status = status + + def request(self, *args, **kwargs): + pass + + def getresponse(self): + return FakeHTTPSResponse(self.status) + + +class FakeHTTPSResponse: + def __init__(self, status): + self.status = status + + def read(self): + if self.status == 200: + return b'{"name":"v0.0.0 2022-03-02","tag_name":"v0.0.0", "html_url":"https://github.com/tangyoha/telegram_media_downloader/releases/tag/v0.0.0"}' + else: + return b"{error}" + + +class MocResponse: + def __init__(self, text: str): + self.text = text + + +def new_request_get(*args, **kwargs): + return MocResponse('{"tag_name":"v0.0.0"}') + + +import unittest +from unittest.mock import MagicMock, patch + +from utils import __version__ +from utils.updates import check_for_updates, get_latest_release + + +class TestUpdates(unittest.TestCase): + @patch("requests.get") + def test_get_latest_release(self, mock_get): + # Mock the response from requests.get + mock_response = MagicMock() + mock_response.text = json.dumps( + { + "name": "v0.0.0 2022-03-02", + "tag_name": "v0.0.0", + "html_url": "https://github.com/tangyoha/telegram_media_downloader/releases/tag/v0.0.0", + } + ) + mock_get.return_value = mock_response + + # Call the function with a test proxy_config + proxy_config = { + "scheme": "http", + "hostname": "localhost", + "port": "8080", + "username": "user", + "password": "pass", + } + result = get_latest_release(proxy_config) + + # Check the result + self.assertEqual(result["name"], "v0.0.0 2022-03-02") + self.assertEqual(result["tag_name"], "v0.0.0") + self.assertEqual( + result["html_url"], + "https://github.com/tangyoha/telegram_media_downloader/releases/tag/v0.0.0", + ) + + @patch("requests.get") + def test_get_latest_release_same_version(self, mock_get): + # Mock the response from requests.get + mock_response = MagicMock() + mock_response.text = json.dumps( + { + "name": f"v{__version__} 2022-03-02", + "tag_name": f"v{__version__}", + "html_url": "https://github.com/tangyoha/telegram_media_downloader/releases/tag/v0.0.0", + } + ) + mock_get.return_value = mock_response + + # Call the function with a test proxy_config + proxy_config = { + "scheme": "http", + "hostname": "localhost", + "port": "8080", + "username": "user", + "password": "pass", + } + result = get_latest_release(proxy_config) + + # Check the result + self.assertEqual(result, {}) + + @patch("requests.get") + def test_get_latest_release_exception(self, mock_get): + # Mock the response from requests.get to raise an exception + mock_get.side_effect = Exception("Test exception") + + # Call the function with a test proxy_config + proxy_config = { + "scheme": "http", + "hostname": "localhost", + "port": "8080", + "username": "user", + "password": "pass", + } + result = get_latest_release(proxy_config) + + # Check the result + self.assertEqual(result, {}) + + @patch("utils.updates.get_latest_release") + @patch("utils.updates.Console") + def test_check_for_updates(self, mock_console, mock_get_latest_release): + # Mock the response from get_latest_release + mock_get_latest_release.return_value = { + "name": "v0.0.0 2022-03-02", + "tag_name": "v0.0.0", + "html_url": "https://github.com/tangyoha/telegram_media_downloader/releases/tag/v0.0.0", + } + + # Call the function with a test proxy_config + proxy_config = { + "scheme": "http", + "hostname": "localhost", + "port": "8080", + "username": "user", + "password": "pass", + } + check_for_updates(proxy_config) + + # Check the console output + mock_console.return_value.print.assert_called_once() diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..165bdb3 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,5 @@ +"""Init namespace""" + +__version__ = "2.2.5" +__license__ = "MIT License" +__copyright__ = "Copyright (C) 2025 tangyoha " diff --git a/utils/crypto.py b/utils/crypto.py new file mode 100644 index 0000000..e8c40fe --- /dev/null +++ b/utils/crypto.py @@ -0,0 +1,77 @@ +"""Crypto utils""" + +import base64 + +from Crypto.Cipher import AES + + +class AesBase64(object): + """for AES encryption""" + + def __init__(self, key: str, iv: str): + self.key = key.encode("utf-8") + self.iv = iv.encode("utf-8") + self.mode = AES.MODE_CBC + + def encrypt(self, content): + """ + Encrypts the given content using the AES encryption algorithm. + + Parameters: + content (str): The content to be encrypted. + + Returns: + str: The encrypted content encoded in base64. + """ + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + content_padding = self.pkcs7padding(content) + encrypt_bytes = cipher.encrypt(content_padding.encode("utf-8")) + return base64.b64encode(encrypt_bytes) + + def decrypt(self, content): + """ + Decrypts the given content using AES encryption + with Cipher Block Chaining (CBC) mode. + + Parameters: + content (str): The content to be decrypted. + + Returns: + str: The decrypted text. + """ + cipher = AES.new(self.key, AES.MODE_CBC, self.iv) + content = base64.b64decode(content) + text = cipher.decrypt(content).decode("utf-8") + return self.pkcs7unpadding(text) + + def pkcs7unpadding(self, text): + """ + Removes the PKCS#7 padding from the given text. + + Parameters: + text (str): The text to remove padding from. + + Returns: + str: The text without PKCS#7 padding. + """ + length = len(text) + unpadding = ord(text[length - 1]) + return text[0 : length - unpadding] + + def pkcs7padding(self, text): + """ + Adds PKCS7 padding to the given text. + + Args: + text (str): The text to be padded. + + Returns: + str: The padded text. + """ + bs = 16 + length = len(text) + bytes_length = len(text.encode("utf-8")) + padding_size = length if (bytes_length == length) else bytes_length + padding = bs - padding_size % bs + padding_text = chr(padding) * padding + return text + padding_text diff --git a/utils/file_management.py b/utils/file_management.py new file mode 100644 index 0000000..1a43896 --- /dev/null +++ b/utils/file_management.py @@ -0,0 +1,78 @@ +"""Utility functions to handle downloaded files.""" +import glob +import os +import pathlib +from hashlib import md5 + + +def get_next_name(file_path: str) -> str: + """ + Get next available name to download file. + + Parameters + ---------- + file_path: str + Absolute path of the file for which next available name to + be generated. + + Returns + ------- + str + Absolute path of the next available name for the file. + """ + posix_path = pathlib.Path(file_path) + counter: int = 1 + new_file_name: str = os.path.join("{0}", "{1}-copy{2}{3}") + while os.path.isfile( + new_file_name.format( + posix_path.parent, + posix_path.stem, + counter, + "".join(posix_path.suffixes), + ) + ): + counter += 1 + return new_file_name.format( + posix_path.parent, + posix_path.stem, + counter, + "".join(posix_path.suffixes), + ) + + +def manage_duplicate_file(file_path: str): + """ + Check if a file is duplicate. + + Compare the md5 of files with copy name pattern + and remove if the md5 hash is same. + + Parameters + ---------- + file_path: str + Absolute path of the file for which duplicates needs to + be managed. + + Returns + ------- + str + Absolute path of the duplicate managed file. + """ + # pylint: disable = R1732 + posix_path = pathlib.Path(file_path) + file_base_name: str = "".join(posix_path.stem.split("-copy")[0]) + name_pattern: str = f"{posix_path.parent}/{file_base_name}*" + # Reason for using `str.translate()` + # https://stackoverflow.com/q/22055500/6730439 + old_files: list = glob.glob( + name_pattern.translate({ord("["): "[[]", ord("]"): "[]]"}) + ) + if file_path in old_files: + old_files.remove(file_path) + current_file_md5: str = md5(open(file_path, "rb").read()).hexdigest() + for old_file_path in old_files: + old_file_md5: str = md5(open(old_file_path, "rb").read()).hexdigest() + if current_file_md5 == old_file_md5: + os.remove(file_path) + return old_file_path + return file_path diff --git a/utils/format.py b/utils/format.py new file mode 100644 index 0000000..eafa088 --- /dev/null +++ b/utils/format.py @@ -0,0 +1,286 @@ +"""util format""" + +import math +import os +import re +import unicodedata +from dataclasses import dataclass +from datetime import datetime +from typing import Optional, Union +from urllib.parse import parse_qs, urlparse + + +@dataclass +class Link: + """Telegram Link""" + + group_id: Union[str, int, None] = None + post_id: Optional[int] = None + comment_id: Optional[int] = None + topic_id: Optional[int] = None + + +def format_byte(size: float, dot=2): + """format byte""" + + # pylint: disable = R0912 + if 0 <= size < 1: + human_size = str(round(size / 0.125, dot)) + "b" + elif 1 <= size < 1024: + human_size = str(round(size, dot)) + "B" + elif math.pow(1024, 1) <= size < math.pow(1024, 2): + human_size = str(round(size / math.pow(1024, 1), dot)) + "KB" + elif math.pow(1024, 2) <= size < math.pow(1024, 3): + human_size = str(round(size / math.pow(1024, 2), dot)) + "MB" + elif math.pow(1024, 3) <= size < math.pow(1024, 4): + human_size = str(round(size / math.pow(1024, 3), dot)) + "GB" + elif math.pow(1024, 4) <= size < math.pow(1024, 5): + human_size = str(round(size / math.pow(1024, 4), dot)) + "TB" + elif math.pow(1024, 5) <= size < math.pow(1024, 6): + human_size = str(round(size / math.pow(1024, 5), dot)) + "PB" + elif math.pow(1024, 6) <= size < math.pow(1024, 7): + human_size = str(round(size / math.pow(1024, 6), dot)) + "EB" + elif math.pow(1024, 7) <= size < math.pow(1024, 8): + human_size = str(round(size / math.pow(1024, 7), dot)) + "ZB" + elif math.pow(1024, 8) <= size < math.pow(1024, 9): + human_size = str(round(size / math.pow(1024, 8), dot)) + "YB" + elif math.pow(1024, 9) <= size < math.pow(1024, 10): + human_size = str(round(size / math.pow(1024, 9), dot)) + "BB" + elif math.pow(1024, 10) <= size < math.pow(1024, 11): + human_size = str(round(size / math.pow(1024, 10), dot)) + "NB" + elif math.pow(1024, 11) <= size < math.pow(1024, 12): + human_size = str(round(size / math.pow(1024, 11), dot)) + "DB" + elif math.pow(1024, 12) <= size: + human_size = str(round(size / math.pow(1024, 12), dot)) + "CB" + else: + raise ValueError( + f'format_byte() takes number than or equal to 0, " \ + " but less than 0 given. {size}' + ) + return human_size + + +class SearchDateTimeResult: + """search result for datetime""" + + def __init__( + self, + value: str = "", + right_str: str = "", + left_str: str = "", + match: bool = False, + ): + self.value = value + self.right_str = right_str + self.left_str = left_str + self.match = match + + +def get_date_time(text: str, fmt: str) -> SearchDateTimeResult: + """Get first of date time,and split two part + + Parameters + ---------- + text: str + ready to search text + + Returns + ------- + SearchDateTimeResult + + """ + res = SearchDateTimeResult() + search_text = re.sub(r"\s+", " ", text) + regex_list = [ + # 2013.8.15 22:46:21 + r"\d{4}[-/\.]{1}\d{1,2}[-/\.]{1}\d{1,2}[ ]{1,}\d{1,2}:\d{1,2}:\d{1,2}", + # "2013.8.15 22:46" + r"\d{4}[-/\.]{1}\d{1,2}[-/\.]{1}\d{1,2}[ ]{1,}\d{1,2}:\d{1,2}", + # "2014.5.11" + r"\d{4}[-/\.]{1}\d{1,2}[-/\.]{1}\d{1,2}", + # "2014.5" + r"\d{4}[-/\.]{1}\d{1,2}", + ] + + format_list = [ + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d", + "%Y-%m", + ] + + for i, value in enumerate(regex_list): + search_res = re.search(value, search_text) + if search_res: + time_str = search_res.group(0) + try: + res.value = datetime.strptime( + time_str.replace("/", "-").replace(".", "-").strip(), format_list[i] + ).strftime(fmt) + except Exception: + break + if search_res.start() != 0: + res.left_str = search_text[0 : search_res.start()] + if search_res.end() + 1 <= len(search_text): + res.right_str = search_text[search_res.end() :] + res.match = True + return res + + return res + + +def replace_date_time(text: str, fmt: str = "%Y-%m-%d %H:%M:%S") -> str: + """Replace text all datetime to the right fmt + + Parameters + ---------- + text: str + ready to search text + + fmt: str + the right datetime format + + Returns + ------- + str + The right format datetime str + + """ + + if not text: + return text + res_str = "" + res = get_date_time(text, fmt) + if not res.match: + return text + if res.left_str: + res_str += replace_date_time(res.left_str) + res_str += res.value + if res.right_str: + res_str += replace_date_time(res.right_str) + + return res_str + + +_BYTE_UNIT = ["B", "KB", "MB", "GB", "TB"] + + +def get_byte_from_str(byte_str: str) -> Optional[int]: + """Get byte from str + + Parameters + ---------- + byte_str: str + Include byte str + + Returns + ------- + int + Byte + """ + search_res = re.match(r"(\d{1,})(B|KB|MB|GB|TB)", byte_str) + if search_res: + unit_str = search_res.group(2) + unit: int = 1 + for it in _BYTE_UNIT: + if it == unit_str: + break + unit *= 1024 + + return int(search_res.group(1)) * unit + + return None + + +def truncate_filename(path: str, limit: int = 230) -> str: + """Truncate filename to the max len. + + Parameters + ---------- + path: str + File name path + + limit: int + limit file name len(utf-8 byte) + + Returns + ------- + str + if file name len more than limit then return truncate filename or return filename + + """ + p, f = os.path.split(os.path.normpath(path)) + f, e = os.path.splitext(f) + f_max = limit - len(e.encode("utf-8")) + f = unicodedata.normalize("NFC", f) + f_trunc = f.encode()[:f_max].decode("utf-8", errors="ignore") + return os.path.join(p, f_trunc + e) + + +def extract_info_from_link(link: str) -> Link: + """Extract info from link""" + if link in ("me", "self"): + return Link(group_id=link) + + try: + u = urlparse(link) + paths = [p for p in u.path.split("/") if p] + query = parse_qs(u.query) + except ValueError: + return Link() + + result = Link() + + if "comment" in query: + result.group_id = paths[0] + result.comment_id = int(query["comment"][0]) + elif len(paths) == 1 and paths[0] != "c": + result.group_id = paths[0] + elif len(paths) == 2: + if paths[0] == "c": + result.group_id = int(f"-100{paths[1]}") + else: + result.group_id = paths[0] + result.post_id = int(paths[1]) + elif len(paths) == 3: + if paths[0] == "c": + result.group_id = int(f"-100{paths[1]}") + result.post_id = int(paths[2]) + else: + result.group_id = paths[0] + result.topic_id = int(paths[1]) + result.post_id = int(paths[2]) + elif len(paths) == 4 and paths[0] == "c": + result.group_id = int(f"-100{paths[1]}") + result.topic_id = int(paths[2]) + result.post_id = int(paths[3]) + + return result + + +def validate_title(title: str) -> str: + """Fix if title validation fails + + Parameters + ---------- + title: str + Chat title + + """ + + r_str = r"[/\\:*?\"<>|\n]" # '/ \ : * ? " < > |' + new_title = re.sub(r_str, "_", title) + return new_title + + +def create_progress_bar(progress, total_bars=10): + """ + example + progress = 50 + progress_bar = create_progress_bar(progress) + print(f'Progress: [{progress_bar}] ({progress}%)') + """ + completed_bars = int(progress * total_bars / 100) + remaining_bars = total_bars - completed_bars + progress_bar = "█" * completed_bars + "░" * remaining_bars + return progress_bar diff --git a/utils/log.py b/utils/log.py new file mode 100644 index 0000000..5d54bbe --- /dev/null +++ b/utils/log.py @@ -0,0 +1,16 @@ +"""Util module to handle logs.""" +import logging + + +class LogFilter(logging.Filter): + """ + Custom Log Filter. + + Ignore logs from specific functions. + """ + + # pylint: disable = W0221 + def filter(self, record): + if record.funcName in ("invoke"): + return False + return True diff --git a/utils/meta.py b/utils/meta.py new file mode 100644 index 0000000..90354be --- /dev/null +++ b/utils/meta.py @@ -0,0 +1,23 @@ +"""Utility module to manage meta info.""" +import platform + +from rich.console import Console + +from . import __copyright__, __license__, __version__ + +APP_VERSION = f"Telegram Media Downloader {__version__}" +DEVICE_MODEL = f"{platform.python_implementation()} {platform.python_version()}" +SYSTEM_VERSION = f"{platform.system()} {platform.release()}" +LANG_CODE = "en" + + +def print_meta(logger): + """Prints meta-data of the downloader script.""" + console = Console() + # pylint: disable = C0301 + console.log( + f"[bold]Telegram Media Downloader v{__version__}[/bold],\n[i]{__copyright__}[/i]" + ) + console.log(f"Licensed under the terms of the {__license__}", end="\n\n") + logger.info(f"Device: {DEVICE_MODEL} - {APP_VERSION}") + logger.info(f"System: {SYSTEM_VERSION} ({LANG_CODE.upper()})") diff --git a/utils/meta_data.py b/utils/meta_data.py new file mode 100644 index 0000000..bd05bf4 --- /dev/null +++ b/utils/meta_data.py @@ -0,0 +1,122 @@ +"""Meta data for download filter""" + + +class ReString: + """for re match""" + + def __init__(self, re_string: str): + self.re_string = re_string + + +class NoneObj: + """for None obj to match""" + + def __init__(self): + pass + + +# pylint: disable=R0902 +# pylint: disable=R0913 +class MetaData: + """ + * `message_date` : - Date the message was sent + * like: message_date > 2022.03.04 && message_date < 2022.03.08 + * `message_id` : - Message 's id + * `media_file_size` : - File size + * `media_width` : - Include photo and video + * `media_height` : - Include photo and video + * `media_file_name` : - file name + * `message_caption` : - message_caption + * `message_duration` : - message_duration + * `sender_id` : - Sender id, empty for messages sent to channels. + * `sender_name` : - Sender name, empty for messages sent to channels. + " `reply_to_message_id` : - reply_to_message_id + """ + + AVAILABLE_MEDIA = ( + "audio", + "document", + "photo", + "sticker", + "animation", + "video", + "voice", + "video_note", + "new_chat_photo", + ) + + def __init__( + self, + message_date: str = None, + message_id: int = None, + message_caption: str = None, + media_file_size: int = None, + media_width: int = None, + media_height: int = None, + media_file_name: str = None, + media_duration: int = None, + media_type: str = None, + file_extension: str = None, + sender_id: int = None, + sender_name: str = None, + reply_to_message_id: int = None, + message_thread_id: int = None, + ): + self.message_date = message_date + self.message_id = message_id + self.message_caption = message_caption + self.media_file_size = media_file_size + self.media_width = media_width + self.media_height = media_height + self.media_file_name = media_file_name + self.media_duration = media_duration + self.media_type = media_type + self.file_extension = file_extension + self.sender_id = sender_id + self.sender_name = sender_name + self.reply_to_message_id = reply_to_message_id + self.message_thread_id = message_thread_id + + def data(self) -> dict: + """Meta map""" + return { + "message_date": self.message_date, + "message_id": self.message_id, + "message_caption": self.message_caption, + "media_file_size": self.media_file_size, + "media_width": self.media_width, + "media_height": self.media_height, + "media_file_name": self.media_file_name, + "media_duration": self.media_duration, + "id": self.message_id, + "caption": self.message_caption, + "file_size": self.media_file_size, + "file_name": self.media_file_name, + "media_type": self.media_type, + "file_extension": self.file_extension, + "sender_id": self.sender_id, + "sender_name": self.sender_name, + "reply_to_message_id": self.reply_to_message_id, + "message_thread_id": self.message_thread_id, + "topic_id": self.message_thread_id, + } + + def export(self) -> dict: + """Export meta data""" + return { + "message_date": self.message_date, + "message_id": self.message_id, + "message_caption": self.message_caption, + "media_file_size": self.media_file_size, + "media_width": self.media_width, + "media_height": self.media_height, + "media_file_name": self.media_file_name, + "media_duration": self.media_duration, + "media_type": self.media_type, + "file_extension": self.file_extension, + "sender_id": self.sender_id, + "sender_name": self.sender_name, + "reply_to_message_id": self.reply_to_message_id, + "message_thread_id": self.message_thread_id, + "topic_id": self.message_thread_id, + } diff --git a/utils/platform.py b/utils/platform.py new file mode 100644 index 0000000..60c1d44 --- /dev/null +++ b/utils/platform.py @@ -0,0 +1,35 @@ +"""for package download""" + +import platform + +# def get_platform() -> str: +# """Get platform title +# Returns +# ------- +# str +# window amd64 return "windows-amd64" +# """ +# sys_platform = platform.system().lower() +# platform_str: str = sys_platform +# if "macos" in sys_platform: +# platform_str = "osx" + +# machine = platform.machine().lower() + +# if "i386" in machine: +# platform_str += "-386" +# else: +# platform_str += "-" + machine + +# return platform_str + + +def get_exe_ext() -> str: + """Get exe ext + Returns + str + if in window then return "exe" other return "" + """ + if "windows" in platform.system().lower(): + return ".exe" + return "" diff --git a/utils/updates.py b/utils/updates.py new file mode 100644 index 0000000..2dfff5e --- /dev/null +++ b/utils/updates.py @@ -0,0 +1,80 @@ +"""Utility module to check for new release of telegram-media-downloader""" +import json + +import requests # type: ignore +from loguru import logger +from rich.console import Console +from rich.markdown import Markdown + +from . import __version__ + + +# pylint: disable = C0301 +def get_latest_release(proxy_config: dict = None) -> dict: + """ + Get the latest release information. + + :param proxy_config: A dictionary containing proxy configuration settings (default: {}). + :type proxy_config: dict + :return: A dictionary containing the latest release information. + :rtype: dict + """ + headers: dict = { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", + } + + if proxy_config: + scheme = proxy_config.get("scheme", "") + hostname = proxy_config.get("hostname", "") + port = proxy_config.get("port", "") + username = proxy_config.get("username") + password = proxy_config.get("password") + + proxies = {} + if proxy_config: + proxies = { + "http": f"{scheme}://{hostname}:{port}", + "https": f"{scheme}://{hostname}:{port}", + } + + if username and password: + proxies["http"] = f"{scheme}://{username}:{password}@{hostname}:{port}" + proxies["https"] = f"{scheme}://{username}:{password}@{hostname}:{port}" + try: + response = requests.get( + url="https://api.github.com/repos/tangyoha/telegram_media_downloader/releases/latest", + headers=headers, + proxies=proxies, + timeout=60, + ) + + except Exception as e: + logger.warning(f"{e}") + return {} + + latest_release: dict = json.loads(response.text) + + if f"v{__version__}" != latest_release["tag_name"]: + return latest_release + + return {} + + +def check_for_updates(proxy_config: dict = None): + """Checks for new releases. + + Using Github API checks for new release and prints information of new release if available. + """ + console = Console() + latest_release = get_latest_release(proxy_config) + try: + if latest_release: + update_message: str = ( + f"## New version of Telegram-Media-Downloader is available - {latest_release['name']}\n" + f"You are using an outdated version v{__version__} please pull in the changes using `git pull` or download the latest release.\n\n" + f"Find more details about the latest release here - {latest_release['html_url']}" + ) + console.print(Markdown(update_message)) + except Exception as e: + logger.warning(f"{e}")