Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c9175d784 |
|
|
@ -1,45 +0,0 @@
|
|||
APP_DEBUG=true
|
||||
APP_ENV=development
|
||||
APP_FALLBACK_LOCALE=en
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=blessingskin
|
||||
DB_USERNAME=username
|
||||
DB_PASSWORD=secret
|
||||
DB_PREFIX=
|
||||
|
||||
# Hash Algorithm for Passwords
|
||||
#
|
||||
# Available values:
|
||||
# - BCRYPT, ARGON2I, PHP_PASSWORD_HASH
|
||||
# - MD5, SALTED2MD5
|
||||
# - SHA256, SALTED2SHA256
|
||||
# - SHA512, SALTED2SHA512
|
||||
#
|
||||
# New sites are *highly* recommended to use BCRYPT.
|
||||
#
|
||||
PWD_METHOD=BCRYPT
|
||||
APP_KEY=base64:JaytOHG/JlLgulTVAhiS0tRqnAfCkQydbdP6VRmoAMY=
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_FROM_NAME=
|
||||
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
PLUGINS_DIR=null
|
||||
PLUGINS_URL=null
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# [Choice] PHP version (use -bullseye variants on local arm64/Apple Silicon): 8, 8.1, 8.0, 7, 7.4, 7.3, 8-bullseye, 8.1-bullseye, 8.0-bullseye, 7-bullseye, 7.4-bullseye, 7.3-bullseye, 8-buster, 8.1-buster, 8.0-buster, 7-buster, 7.4-buster
|
||||
ARG VARIANT=8-bullseye
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/php:0-${VARIANT}
|
||||
|
||||
# Install MariaDB client
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get install -y mariadb-client zlib1g-dev libpng-dev libzip-dev libwebp-dev \
|
||||
&& apt-get clean -y && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install php-mysql driver
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql gd zip
|
||||
|
||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||
ARG NODE_VERSION="none"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# Enable Apache rewrite module
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
Listen 8080
|
||||
|
||||
<Directory /workspace/public/>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<VirtualHost *:8080>
|
||||
DocumentRoot /workspace/public
|
||||
ErrorLog /workspace/storage/logs/apache-error.log
|
||||
CustomLog /workspace/storage/logs/apache-access.log combined
|
||||
</VirtualHost>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.231.6/containers/php-mariadb
|
||||
// Update the VARIANT arg in docker-compose.yml to pick a PHP version
|
||||
{
|
||||
"name": "PHP & MariaDB (Community)",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"xdebug.php-debug",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"mrmlnc.vscode-apache"
|
||||
],
|
||||
|
||||
// For use with PHP or Apache (e.g.php -S localhost:8080 or apache2ctl start)
|
||||
"forwardPorts": [8080, 3306],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "sudo truncate -s 0 /etc/apache2/ports.conf && sudo rm -f /etc/apache2/sites-enabled/000-default.conf && sudo ln -sf /workspace/.devcontainer/blessing-skin.apache.conf /etc/apache2/sites-enabled/ && ln -sf .devcontainer/.env.devcontainer .env && composer install && yarn install",
|
||||
|
||||
// Start apache2 after the container is started
|
||||
"postStartCommand": "apache2ctl start && echo '\\n👉 \\e[0;32mPlease run '\\'yarn build\\'' to build the frontend. Application is available on port 8080.\\e[0m 👈\\n'",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
"powershell": "latest"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a version of PHP version: 8, 8.1, 8.0, 7, 7.4
|
||||
# Append -bullseye or -buster to pin to an OS version.
|
||||
# Use -bullseye variants on local arm64/Apple Silicon.
|
||||
VARIANT: "8-bullseye"
|
||||
# Optional Node.js version
|
||||
NODE_VERSION: "lts/*"
|
||||
|
||||
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: vscode
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: mariadb:10.4
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mariadb-data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: mariadb
|
||||
MYSQL_DATABASE: blessingskin
|
||||
MYSQL_USER: username
|
||||
MYSQL_PASSWORD: secret
|
||||
|
||||
# Add "forwardPorts": ["3306"] to **devcontainer.json** to forward MariaDB locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
volumes:
|
||||
mariadb-data:
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
.git/
|
||||
.github/
|
||||
.vscode/
|
||||
.idea/
|
||||
.cache/
|
||||
.cache-loader/
|
||||
coverage/
|
||||
node_modules/
|
||||
plugins/**
|
||||
public/app/*
|
||||
public/lang/*
|
||||
public/plugins/**
|
||||
resources/assets/tests/
|
||||
storage/*.db
|
||||
storage/*.sqlite
|
||||
storage/insane-profile-cache
|
||||
storage/oauth-public.key
|
||||
storage/oauth-private.key
|
||||
storage/install.lock
|
||||
storage/options.php
|
||||
storage/debugbar
|
||||
storage/framework/cache/**
|
||||
storage/framework/sessions/**
|
||||
storage/framework/testing/
|
||||
storage/framework/views/**
|
||||
storage/logs/**
|
||||
storage/packages/**
|
||||
storage/textures/*
|
||||
storage/update_cache/*
|
||||
target/
|
||||
tests/
|
||||
vendor/*
|
||||
_ide_helper.php
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.env
|
||||
.env.testing
|
||||
.eslintignore
|
||||
.eslintrc.yml
|
||||
.gitignore
|
||||
.php_cs.*
|
||||
.php-cs-fixer.cache
|
||||
.php-cs-fixer.dist.php
|
||||
.phpstorm.meta.php
|
||||
.phpunit.result.cache
|
||||
.sass-cache
|
||||
.uini
|
||||
azure-pipelines.yml
|
||||
crowdin.yml
|
||||
docker-compose.yml
|
||||
Dockerfile*
|
||||
index.html
|
||||
junit.xml
|
||||
phpunit.xml
|
||||
README*.md
|
||||
server.php
|
||||
tsconfig.dev.json
|
||||
tsconfig.eslint.json
|
||||
yarn-error.log
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{php,md,ps1,Dockerfile}]
|
||||
indent_size = 4
|
||||
79
.env.example
79
.env.example
|
|
@ -1,45 +1,46 @@
|
|||
APP_DEBUG=false
|
||||
APP_ENV=production
|
||||
APP_FALLBACK_LOCALE=en
|
||||
###################################
|
||||
# Blessing Skin Server V3 #
|
||||
# Configuration #
|
||||
###################################
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=blessingskin
|
||||
DB_USERNAME=username
|
||||
DB_PASSWORD=secret
|
||||
DB_PREFIX=
|
||||
# 务必在生产环境下禁用!
|
||||
APP_DEBUG = true
|
||||
|
||||
# Hash Algorithm for Passwords
|
||||
# =========================
|
||||
# = 数据库连接信息 =
|
||||
# =========================
|
||||
# MySQL 主机
|
||||
DB_HOST = 127.0.0.1
|
||||
# MySQL 端口,默认 3306
|
||||
DB_PORT = 3306
|
||||
# MySQL 数据库名
|
||||
DB_DATABASE = ""
|
||||
|
||||
# MySQL 数据库用户名
|
||||
DB_USERNAME = ""
|
||||
# MySQL 连接密码
|
||||
DB_PASSWORD = ""
|
||||
# =========================
|
||||
|
||||
# 数据表前缀
|
||||
#
|
||||
# Available values:
|
||||
# - BCRYPT, ARGON2I, PHP_PASSWORD_HASH
|
||||
# - MD5, SALTED2MD5
|
||||
# - SHA256, SALTED2SHA256
|
||||
# - SHA512, SALTED2SHA512
|
||||
# 如果您有在同一数据库内安装多个 Blessing Skin Server 的需求,
|
||||
# 请为每个皮肤站设置不同的数据表前缀。前缀名只能为数字、字母加下划线。
|
||||
DB_PREFIX = ""
|
||||
|
||||
# 密码加密方式
|
||||
#
|
||||
# New sites are *highly* recommended to use BCRYPT.
|
||||
#
|
||||
PWD_METHOD=BCRYPT
|
||||
APP_KEY=
|
||||
# 可选的值有:MD5, SALTED2MD5, SHA256
|
||||
PWD_METHOD = "MD5"
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_FROM_NAME=
|
||||
# 盐,用于 token 与密码加密
|
||||
# 修改为任意随机字符串以保证站点安全
|
||||
SALT = "change-it+to*what)you^like"
|
||||
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
PLUGINS_DIR=null
|
||||
PLUGINS_URL=null
|
||||
# SMTP 配置,用于发送重置密码的邮件
|
||||
# MAIL_HOST 留空以停用重置密码功能
|
||||
MAIL_HOST = ""
|
||||
MAIL_PORT = 465
|
||||
MAIL_USERNAME = ""
|
||||
MAIL_PASSWORD = ""
|
||||
MAIL_ENCRYPTION = "ssl"
|
||||
|
|
|
|||
105
.env.testing
105
.env.testing
|
|
@ -1,105 +0,0 @@
|
|||
APP_DEBUG=false
|
||||
APP_ENV=testing
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=:memory:
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
DB_PREFIX=
|
||||
|
||||
PWD_METHOD=BCRYPT
|
||||
BCRYPT_ROUNDS=4
|
||||
APP_KEY=base64:eVX/xzF5NhpGB2luswliFx9XSBsbbAP21wOi68X/P34=
|
||||
|
||||
MAIL_MAILER=array
|
||||
MAIL_HOST=localhost
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=ssl
|
||||
|
||||
CACHE_DRIVER=array
|
||||
SESSION_DRIVER=array
|
||||
QUEUE_CONNECTION=sync
|
||||
LOG_CHANNEL=null
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=
|
||||
REDIS_PORT=6379
|
||||
|
||||
PLUGINS_DIR=plugins
|
||||
PLUGINS_URL=
|
||||
|
||||
TEXTURES_DIR=
|
||||
|
||||
JWT_SECRET=1tdM3gXarxYI4KlAHMBo238iC2tEb4I3EtBlZTQQXvInXIt7V2ix7hJ1KTvxCKZW
|
||||
|
||||
PASSPORT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
|
||||
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC6q6SCprX3yfOE
|
||||
DFBnfFk3R+33qvoe26nYQkavKfv7zA9KQxCBNHEsFKOQ6ui+ViebVHAIHBPm2518
|
||||
REVMLN2JONvXbPETV6gJO/b6FFwo2Aow/GbTnesLhWEAPW11ei0/hBbjWF9hQZ/n
|
||||
x3YsFk0xtml2iPDijfUohwp50iFyCQylw4S5Sy3vuVdM063dkxvECsU6wmHDev9C
|
||||
PxFZGl3W2iLSwttYl7xmlwll8xuqxDQUJpJbOxrPeDKdDI1ikarSqA1c8bV1YLT+
|
||||
CHxB7T5b1EPeaYRmeLl+wyd/ZxeBWWgDLBusi5wPFpSEIVxu1RzTYarOXEpD+XNV
|
||||
Ohpb7LxpbJx8YRJru1B6CBouVO2pqoCEM21GtG7zSDNtaY+yVcj9Kf2SIbfz0e6a
|
||||
SLAcyOT0q3aId4+z9q8TAfPVV7y0D7B3PaOz/3pMC+jHDjCRwQkN/DR7ODUKEjCd
|
||||
UjsveGHtseBa3qBoKWcg39ZvwYeDF0P/cFa/yOqw5JxyWhbjk95YdPaSixdUyyMG
|
||||
XkFTOrnBREVtBAfdTOG6WTFcIlyJK+ST0cJXVcrjFonbmJCCwTJqxz9t+935CfTt
|
||||
CzLLPdONU16jZJ7j1cVoWb4L8o0OA+FBSawBxFOKyhFlh3+HKRnSCZSDqaSDsYUf
|
||||
M3IupiGUcByhpZ9mNqhnLeivIXwapwIDAQABAoICAFu27F+S2DH0A9S7lh+aPV1H
|
||||
VniKhVR2+aZ6va7fUmJ++n4yoB/TK82MIGcZu5uUyeXr4RVi8jZJbcF565BHNNtw
|
||||
V7cq2/F0bmeHEkwBh9w7dRpnUIAlhS/GawfKpoaDLksYM4SkzUwECbQ/0GRN2sST
|
||||
ipKGKtAtDihI3RFIeE1Ge/PPsdy2Ps4bAnUJRdHpLsmtvwSlL5JzUonyYawlI7jl
|
||||
uRlTSqDnAFZpW+E+xjerKalC4EK5se0AceGuoqKszkCs98/UJCMVDigH9EER9sL4
|
||||
chYLQtVz+DN7X+MdPDO9wThZygkHGPhi0DpxB7CevYhv4pN8TbLDE3Lq1ruWf2T3
|
||||
7ts21ymjVIiOayBA9l86P0FSS/lP9KF53LyeNkOJKUy5On6xHoW5IKlrMJdmGwFH
|
||||
B4yaR7bw5vxErhpMTzcYJVWqjCbo+PBJhdy7x+2XrrBLs9X0hfS/jeeAIuRlzju1
|
||||
9xe3zO6U41sDjkCkrUavOn57DL6jh9LMgxT8cZkSdrP6rpawEyjPUi5kMbbQkv0j
|
||||
eWiqz0vozJN5HcVpj36F5kqZyCnIojmeo4FCKdn7n/wvyGYQPSAekVpV80KzoJ2j
|
||||
GQ440Q7Sgozj/Lw4cCPgG3/MA1Dwu+TUuaddFjBH2oqZ2X0bCqVCEVWhfmaD4z2R
|
||||
I9C9nLvpxoMtcCHkG1VFAoIBAQDfawcBHMPxZQOy/qTqXZd7YR3bzGgd/SkLEWwg
|
||||
PtDGKe36tDf3E/RGRij4HGl9v/fA7N/CufW0tJY7Ii/cn7yYdZg/dzaCoYIZbICl
|
||||
CytWtsM0iH3XPuY3UpgsOwML0xK8hdD1U7qBKk5rnjF9Q9vKrB8ATR9hM0bPaGtr
|
||||
Bqfx8A+kj6wqRpA8jbN0kZxJVv0/LgZSCrH3qUjHfXoYYtLhMBZd8UydyvyETo/1
|
||||
Z44WS9oNqX8mBUvHjsuOQx3+eFPOr69QPIo06QMxytSEzMilgz40QUBWF68gKwGi
|
||||
NYdUfR3IXVTmvJhYH2mQWqMKVk+KJFd2UanjKbBCOKrDSG4TAoIBAQDV5LMp/ztd
|
||||
YQvMCWJzUrpazGqkoEGli/qxWb/pDpemgQT++lt6PBRQmmLKXZfNp9VJdZdP6+lF
|
||||
ypGcA8tACY93m7Fk4wtewXG+0oTxmBkWqSiiO3ExoBQTxXLoZ9GwKt/exaM25QJ1
|
||||
O2livxrYFFJbUe1YRqQENIURYk13RgeIaWS0gd9vp/yp0EhZAvGUPHFjKRsLjw7Y
|
||||
gZDJ+lXj2pXg9THUzkhVDm+fM6blIsLcvf7qc8yKQI3nwZr00e/ba7xSNF8AhpdP
|
||||
rxw59vm1RzsngZpZOK0Z1143gFRtGhhVWtvmhCvJo1EOssFu01ixE0bNq5nRIIJo
|
||||
O/mY7NC9bCOdAoIBAEBfcyYz5pUwGM/DJTtN+i6XfeXt0HYLkn7Y50GnN7pRLHuW
|
||||
36U2P6Tb5EQQ06hi3nzdA1/0+sG1Yq/pGsdD0zBOea6Xp8IdzQGMTMjBHhyfDkGd
|
||||
rjyNqAF6r9PWsPsANx7Qo7N8C3nZ+bxyWSoRmkucKlaI4ii8gIOUP5cX1N4V4Dv3
|
||||
FZEcwcRgw7srlU9gXBmPJk0PPdXxFcI8+if6mW4+z8MDmqLAcN+iT0JTMxJjipFz
|
||||
K+qFjh8Smr4Dwqmme+dKoYXJ27yBAuWe3nrhElL2LL8bqfDkZBYtrgvRxotmfWVU
|
||||
1vigkHibnGv2YZHB6qsP649w2jVUtq9t6m3X+bcCggEAEKDqCN7N17GewCsOm1aY
|
||||
JEz2EXxf/iXGxJjsoYq/4XLwV35RNEyNa8LE4WSrU5KzszVQISd/CCz6av2khIL5
|
||||
w1u4S9aW4LP7StGFAl9HvApEnXAvmaMPTIYyK70+gQqkQuZsjOz65vBKfiHLTXcu
|
||||
++h/ojhDsgv/OF3DFf28wi8nZB0gqMaPjwghR8JB07trOUFN1/U0O0K/ZeRvXvp0
|
||||
YnvNdvTejLZFmUPjuracHZsrwUBla24fWiAkEtprYkya5G0r4ZeVFd3QPPVlbmFu
|
||||
SOD7heoxEuw6Z+gzKBQ6RhB9PguSd+eZeqINBbeqkoGkJIMtvyNe4AmhmvD2PXO1
|
||||
xQKCAQAx12vD3q2+DHrpJ4fKGp1RbCxFIOYgXcBJ6zCozVZXpLUa54IBTLyQyDNF
|
||||
D8+Wq5b1IbCnxBn55tsgq/CSLkVHigVfUDGnAt3lD+ggLdZmu7ayQY0BNe1quF5M
|
||||
EgkOE0uYtq+2p+u4gxRB+gnQd1YIlPs0U0mrawbzV5ZX2vR5Ry1XpBno75JpnUbN
|
||||
3D9DuIDVLuqsKJcx6KClWef2PzJB2uN7jDf4UnCtp5QCFB1GNt+5AzCNh9Bozas8
|
||||
OflASrSn64AeyCZycCplmRY/F4BnH++7YI0Q/mjawByW7qYoHkFzmKuUcgakh200
|
||||
y/ieeWy8Vunl0e4T4Bz/0zInBifn
|
||||
-----END PRIVATE KEY-----"
|
||||
|
||||
PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuqukgqa198nzhAxQZ3xZ
|
||||
N0ft96r6Htup2EJGryn7+8wPSkMQgTRxLBSjkOrovlYnm1RwCBwT5tudfERFTCzd
|
||||
iTjb12zxE1eoCTv2+hRcKNgKMPxm053rC4VhAD1tdXotP4QW41hfYUGf58d2LBZN
|
||||
MbZpdojw4o31KIcKedIhcgkMpcOEuUst77lXTNOt3ZMbxArFOsJhw3r/Qj8RWRpd
|
||||
1toi0sLbWJe8ZpcJZfMbqsQ0FCaSWzsaz3gynQyNYpGq0qgNXPG1dWC0/gh8Qe0+
|
||||
W9RD3mmEZni5fsMnf2cXgVloAywbrIucDxaUhCFcbtUc02GqzlxKQ/lzVToaW+y8
|
||||
aWycfGESa7tQeggaLlTtqaqAhDNtRrRu80gzbWmPslXI/Sn9kiG389HumkiwHMjk
|
||||
9Kt2iHePs/avEwHz1Ve8tA+wdz2js/96TAvoxw4wkcEJDfw0ezg1ChIwnVI7L3hh
|
||||
7bHgWt6gaClnIN/Wb8GHgxdD/3BWv8jqsOSccloW45PeWHT2kosXVMsjBl5BUzq5
|
||||
wURFbQQH3UzhulkxXCJciSvkk9HCV1XK4xaJ25iQgsEyasc/bfvd+Qn07Qsyyz3T
|
||||
jVNeo2Se49XFaFm+C/KNDgPhQUmsAcRTisoRZYd/hykZ0gmUg6mkg7GFHzNyLqYh
|
||||
lHAcoaWfZjaoZy3oryF8GqcCAwEAAQ==
|
||||
-----END PUBLIC KEY-----"
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
public/
|
||||
vendor/
|
||||
coverage/
|
||||
plugins/
|
||||
node_modules/
|
||||
*.d.ts
|
||||
resources/assets/tests/__mocks__/
|
||||
resources/assets/tests/ts-shims/
|
||||
resources/assets/tests/*.ts
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
root: true
|
||||
parser: '@typescript-eslint/parser'
|
||||
parserOptions:
|
||||
project: tsconfig.eslint.json
|
||||
plugins:
|
||||
- '@typescript-eslint/eslint-plugin'
|
||||
extends:
|
||||
- eslint:recommended
|
||||
- plugin:@typescript-eslint/recommended
|
||||
- plugin:@typescript-eslint/recommended-requiring-type-checking
|
||||
- plugin:react-hooks/recommended
|
||||
rules:
|
||||
prefer-const: error
|
||||
'@typescript-eslint/no-unsafe-assignment': off
|
||||
'@typescript-eslint/no-unsafe-member-access': off
|
||||
'@typescript-eslint/no-unsafe-return': off
|
||||
'@typescript-eslint/no-unused-vars': off
|
||||
'@typescript-eslint/explicit-module-boundary-types': off
|
||||
'@typescript-eslint/no-explicit-any': off
|
||||
'@typescript-eslint/ban-ts-comment': off
|
||||
'@typescript-eslint/no-non-null-assertion': off
|
||||
'@typescript-eslint/no-floating-promises': off
|
||||
'@typescript-eslint/no-misused-promises':
|
||||
- off
|
||||
- checksVoidReturn: false
|
||||
'@typescript-eslint/unbound-method': off
|
||||
'@typescript-eslint/restrict-template-expressions': off
|
||||
81
.github/CONTRIBUTING.md
vendored
81
.github/CONTRIBUTING.md
vendored
|
|
@ -1,81 +0,0 @@
|
|||
# 贡献指南
|
||||
|
||||
欢迎您为 Blessing Skin 作出贡献!
|
||||
|
||||
## 分支约定
|
||||
|
||||
不管是直接 push 代码还是提交 Pull Request,都必须使 commit 指向 `dev` 分支。
|
||||
|
||||
## 开发
|
||||
|
||||
### 环境设置
|
||||
|
||||
首先确保您安装好以下工具:
|
||||
|
||||
- [Git](https://git-scm.org)
|
||||
- [Node.js](https://nodejs.org)
|
||||
- [Yarn](https://yarnpkg.com)
|
||||
- [Composer](https://getcomposer.org)
|
||||
- [PowerShell Core](https://github.com/PowerShell/PowerShell#get-powershell)
|
||||
|
||||
然后执行以下命令:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bs-community/blessing-skin-server.git
|
||||
cd blessing-skin-server
|
||||
composer install
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
yarn
|
||||
```
|
||||
|
||||
然后在 `.env` 中配置好您的环境信息,务必设置好 `ASSET_URL`,否则无法编译前端资源。
|
||||
|
||||
### 进行开发
|
||||
|
||||
运行 Blessing Skin 前,前端代码需要并构建。
|
||||
|
||||
当 `.env` 中的 `APP_ENV` 为 `development` 时,您需要先执行 `yarn dev` 并保持此进程的运行。这样 Blessing Skin 的前端资源才能被正确加载,同时使页面带有热重载功能。(有时热重载可能会失效,此时需要您手动刷新页面)
|
||||
|
||||
另外,在运行 `yarn dev` 即运行 `webpack-dev-server` 时,由于 `webpack-dev-server` 的端口往往与 Blessing Skin 的端口不同,因此有可能导致热重载失败。此时可以在 Nginx 中添加以下配置:
|
||||
|
||||
```
|
||||
location ~* \w+\.hot-update\.json$ {
|
||||
rewrite (\w+\.hot-update\.json)$ /$1 break;
|
||||
proxy_pass http://$host:8080;
|
||||
}
|
||||
```
|
||||
|
||||
当 `APP_ENV` 为其它值时,您需要事先执行 `pwsh ./tools/build.ps1`。此命令将构建并压缩前端资源。通常用于生产环境。
|
||||
|
||||
> 如果传递 `-Simple` 参数给 `build.ps1` 脚本,则只会运行 webpack 来编译代码,而不会复制首页背景以及生成 commit 信息。
|
||||
|
||||
### 测试
|
||||
|
||||
进行前端测试:
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
请尽量保证前端测试的覆盖率为 100%。
|
||||
|
||||
进行 PHP 代码测试:
|
||||
|
||||
```bash
|
||||
./vendor/bin/phpunit
|
||||
```
|
||||
|
||||
## 代码规范
|
||||
|
||||
请确保您的编辑器或 IDE 安装好 EditorConfig 插件。如果进行前端开发,推荐安装上 ESLint 插件。(您也可以通过执行 `yarn lint` 进行检查)
|
||||
|
||||
## 发布
|
||||
|
||||
> 本节仅针对本项目的维护成员。
|
||||
|
||||
首先请确保您当前处于 `dev` 分支。然后,运行 `yarn new-version <action>` 即可发布新版本,不需要其它人工操作。
|
||||
|
||||
其中 `action` 参数是必需的,且只能为 `patch`、`minor`、`major` 中的其中一个。
|
||||
|
||||
另外,可以不定期地将 `dev` 上的 commits 合并到 `master` 分支,以满足一些想尝鲜的用户。但尽管如此,这不意味着 `dev` 分支是随意的—— `dev` 分支上的功能、特性可以是未完成的,但不应该影响用户的使用,因为也允许用户使用 `dev` 分支上的代码去体验新特性。
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
|||
custom: https://afdian.net/@blessing-skin
|
||||
71
.github/ISSUE_TEMPLATE/bug-report-zh.yml
vendored
71
.github/ISSUE_TEMPLATE/bug-report-zh.yml
vendored
|
|
@ -1,71 +0,0 @@
|
|||
name: Bug 报告
|
||||
description: 发起 bug 报告
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
在报告问题之前,请确保您已经 **认真** 阅读:
|
||||
- [FAQ](https://blessing.netlify.app/en/faq.html)
|
||||
- [报告问题的正确姿势](https://blessing.netlify.app/report.html)
|
||||
- type: input
|
||||
id: bs
|
||||
attributes:
|
||||
label: Blessing Skin 版本
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: php
|
||||
attributes:
|
||||
label: PHP 版本
|
||||
options:
|
||||
- '7.3'
|
||||
- '7.4'
|
||||
- '8.0'
|
||||
- '8.1'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: 出现问题时所使用的浏览器
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: web-server
|
||||
attributes:
|
||||
label: 您正在使用的 Web Server
|
||||
options:
|
||||
- Nginx
|
||||
- Apache
|
||||
- type: checkboxes
|
||||
id: baota
|
||||
attributes:
|
||||
label: 您正在使用宝塔吗?
|
||||
options:
|
||||
- label: 是
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: 出现了什么问题?
|
||||
description: 顺便告诉我们,您期望的行为是怎样的?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 错误日志
|
||||
description: 您可以粘贴 Blessing Skin 的日志或 Web Server 的日志。Blessing Skin 的日志位于 `storage/logs` 目录里。
|
||||
render: text
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: 详细描述您出错前的操作步骤
|
||||
validations:
|
||||
required: true
|
||||
64
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
64
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
|
@ -1,64 +0,0 @@
|
|||
name: Bug Report
|
||||
description: File a bug report
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please filing an issue, please make sure you've read:
|
||||
- [FAQ](https://blessing.netlify.app/en/faq.html)
|
||||
- type: input
|
||||
id: bs
|
||||
attributes:
|
||||
label: Which version of Blessing Skin are you using?
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: php
|
||||
attributes:
|
||||
label: Which version of PHP are you using?
|
||||
options:
|
||||
- '7.3'
|
||||
- '7.4'
|
||||
- '8.0'
|
||||
- '8.1'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: web-server
|
||||
attributes:
|
||||
label: Which web server are you using?
|
||||
options:
|
||||
- Nginx
|
||||
- Apache
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error Logs
|
||||
description: You can paste logs of Blessing Skin or your web server. Logs of Blessing Skin can be found at `storage/logs` directory.
|
||||
render: text
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Tell us how to reproduce this issue.
|
||||
validations:
|
||||
required: true
|
||||
20
.github/ISSUE_TEMPLATE/config.yml
vendored
20
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,20 +0,0 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ask questions about using Blessing Skin
|
||||
url: https://github.com/bs-community/blessing-skin-server/discussions
|
||||
about: If you're not going to report a bug, please ask and answer questions there.
|
||||
- name: Report Issue about Blessing Skin plugins
|
||||
url: https://github.com/bs-community/blessing-skin-plugins/issues
|
||||
about: Please ask and answer questions there.
|
||||
- name: Report Issue about integrating with Flarum
|
||||
url: https://github.com/bs-community/flarum-oauth-client/issues
|
||||
about: Please ask and answer questions there.
|
||||
- name: 询问关于使用 Blessing Skin 的问题
|
||||
url: https://github.com/bs-community/blessing-skin-server/discussions
|
||||
about: 如果您并不是要报告 bug,请在那里进行讨论。
|
||||
- name: 报告与 Blessing Skin 插件有关的问题
|
||||
url: https://github.com/bs-community/blessing-skin-plugins/issues
|
||||
about: 请在那里报告问题。
|
||||
- name: 报告与 Flarum 对接有关的问题
|
||||
url: https://github.com/bs-community/flarum-oauth-client/issues
|
||||
about: 请在那里报告问题。
|
||||
155
.github/workflows/CI.yml
vendored
155
.github/workflows/CI.yml
vendored
|
|
@ -1,155 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths-ignore:
|
||||
- 'resources/lang/**'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths-ignore:
|
||||
- 'resources/lang/**'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
php-lint:
|
||||
name: PHP Linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
coverage: none
|
||||
extensions: mbstring, dom, fileinfo, gd, imagick
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
composer install --prefer-dist --no-progress
|
||||
- name: Prepare
|
||||
run: |
|
||||
cp .env.example .env
|
||||
mkdir -p resources/views/overrides
|
||||
- name: Validate Twig templates
|
||||
run: php artisan twig:lint -v
|
||||
- name: Check coding style
|
||||
run: ./vendor/bin/php-cs-fixer fix --dry-run --stop-on-violation --diff --format=txt
|
||||
php:
|
||||
name: PHP ${{ matrix.php }} Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['8.2', '8.3']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup PHP only
|
||||
uses: shivammathur/setup-php@v2
|
||||
if: matrix.php != '8.3'
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: none
|
||||
extensions: mbstring, dom, fileinfo, sqlite, gd, zip, imagick
|
||||
- name: Setup PHP with Xdebug
|
||||
uses: shivammathur/setup-php@v2
|
||||
if: matrix.php == '8.3'
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: xdebug
|
||||
extensions: mbstring, dom, fileinfo, sqlite, gd, zip, imagick
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
- name: Install Composer dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
- name: Run tests only
|
||||
if: matrix.php != '8.3'
|
||||
run: ./vendor/bin/phpunit
|
||||
- name: Run tests with coverage report
|
||||
if: matrix.php == '8.3'
|
||||
run: ./vendor/bin/phpunit --coverage-clover=coverage.xml
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v1
|
||||
if: matrix.php == '8.3' && success()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
name: github-actions
|
||||
lint:
|
||||
name: Frontend Linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Run checks
|
||||
run: |
|
||||
yarn lint
|
||||
yarn fmt:check
|
||||
yarn type:check
|
||||
jest:
|
||||
name: Frontend Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Run tests
|
||||
run: yarn test --coverage
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
name: github-actions
|
||||
build:
|
||||
name: Snapshot Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.2
|
||||
coverage: none
|
||||
extensions: mbstring, dom, fileinfo, sqlite, gd, zip, imagick
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache Node dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-yarn-lock-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-lock-
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
composer install --prefer-dist --no-progress --no-dev
|
||||
yarn install --frozen-lockfile
|
||||
- name: Build frontend
|
||||
run: |
|
||||
yarn build
|
||||
cp resources/assets/src/images/bg.webp public/app/
|
||||
cp resources/assets/src/images/favicon.ico public/app/
|
||||
- uses: benjlevesque/short-sha@v3.0
|
||||
id: short-sha
|
||||
- name: Archive release
|
||||
run: zip -9 -r blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip app bootstrap config database plugins public resources/lang resources/views resources/misc/textures routes storage vendor .env.example artisan LICENSE README.md README-zh.md index.html
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip
|
||||
path: blessing-skin-server-${{ steps.short-sha.outputs.sha }}.zip
|
||||
40
.github/workflows/Release.yml
vendored
40
.github/workflows/Release.yml
vendored
|
|
@ -1,40 +0,0 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Build and create archive
|
||||
run: ./tools/release.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
- name: Upload release asset
|
||||
id: upload_release_asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./blessing-skin-server-${{ steps.get_version.outputs.VERSION }}.zip
|
||||
asset_name: blessing-skin-server-${{ steps.get_version.outputs.VERSION }}.zip
|
||||
asset_content_type: application/zip
|
||||
37
.github/workflows/Telegram.yml
vendored
37
.github/workflows/Telegram.yml
vendored
|
|
@ -1,37 +0,0 @@
|
|||
name: Telegram
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'bootstrap/**'
|
||||
- 'config/**'
|
||||
- 'database/**'
|
||||
- 'public/**'
|
||||
- 'resources/**'
|
||||
- 'routes/**'
|
||||
- '*.lock'
|
||||
- 'webpack.*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
notification:
|
||||
name: Send Message
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download bot
|
||||
run: |
|
||||
$headers = @{ Authorization = 'Bearer ${{ secrets.GITHUB_TOKEN }}' }
|
||||
$botRelease = (Invoke-WebRequest -Headers $headers 'https://api.github.com/repos/bs-community/telegram-bot/releases/latest').Content | ConvertFrom-Json
|
||||
$botBinUrl = ((Invoke-WebRequest -Headers $headers $botRelease.assets_url).Content | ConvertFrom-Json).browser_download_url
|
||||
bash -c "curl --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -fSL $botBinUrl -o bot"
|
||||
chmod +x ./bot
|
||||
shell: pwsh
|
||||
- name: Run bot
|
||||
run: ./bot diff
|
||||
shell: pwsh
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
35
.gitignore
vendored
35
.gitignore
vendored
|
|
@ -1,29 +1,14 @@
|
|||
.env
|
||||
.sass-cache
|
||||
coverage
|
||||
.idea/
|
||||
.cache/
|
||||
.cache-loader/
|
||||
vendor/*
|
||||
storage/textures
|
||||
storage/textures/*
|
||||
storage/update_cache/*
|
||||
textures/*
|
||||
node_modules/*
|
||||
target/
|
||||
yarn-error.log
|
||||
_ide_helper.php
|
||||
.phpstorm.meta.php
|
||||
.uini
|
||||
junit.xml
|
||||
storage/*.db
|
||||
storage/*.sqlite
|
||||
storage/insane-profile-cache
|
||||
storage/oauth-public.key
|
||||
storage/oauth-private.key
|
||||
storage/install.lock
|
||||
storage/options.php
|
||||
.phpunit.result.cache
|
||||
.php-cs-fixer.cache
|
||||
resources/views/overrides
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
resources/cache/*
|
||||
assets/bower_components/*
|
||||
assets/dist/*
|
||||
koala-config.json
|
||||
assets/css/*
|
||||
assets/js/*
|
||||
bootstrap/*
|
||||
resources/assets/*
|
||||
storage/*
|
||||
|
|
|
|||
41
.gitpod.yml
41
.gitpod.yml
|
|
@ -1,41 +0,0 @@
|
|||
tasks:
|
||||
- init: yarn install
|
||||
command: yarn dev
|
||||
- init: composer install
|
||||
command: |
|
||||
cp .env.example .env
|
||||
mkdir public/app/
|
||||
cp resources/assets/src/images/bg.webp resources/assets/src/images/favicon.ico public/app
|
||||
touch storage/database.db
|
||||
sed 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' -i .env
|
||||
sed 's/DB_DATABASE=blessingskin/DB_DATABASE=\/workspace\/blessing-skin-server\/storage\/database\.db/' -i .env
|
||||
php artisan key:generate
|
||||
php artisan serve --host=0.0.0.0
|
||||
- command: gp ports await 8080 && gp preview $(gp url 8000)
|
||||
|
||||
github:
|
||||
prebuilds:
|
||||
# enable for the master/default branch (defaults to true)
|
||||
master: true
|
||||
# enable for all branches in this repo (defaults to false)
|
||||
branches: false
|
||||
# enable for pull requests coming from this repo (defaults to true)
|
||||
pullRequests: true
|
||||
# add a check to pull requests (defaults to true)
|
||||
addCheck: true
|
||||
# add a "Review in Gitpod" button as a comment to pull requests (defaults to false)
|
||||
addComment: false
|
||||
|
||||
vscode:
|
||||
extensions:
|
||||
- 'editorconfig.editorconfig'
|
||||
- 'eamodio.gitlens'
|
||||
- 'bmewburn.vscode-intelephense-client'
|
||||
- 'esbenp.prettier-vscode'
|
||||
- 'jpoissonnier.vscode-styled-components'
|
||||
- 'mblode.twig-language-2'
|
||||
- 'felixfbecker.php-debug'
|
||||
|
||||
ports:
|
||||
- port: 8080
|
||||
visibility: public
|
||||
12
.htaccess
Normal file
12
.htaccess
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
RewriteEngine On
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
RewriteRule ^.*$ index.php [L]
|
||||
|
||||
# Protect .env file
|
||||
<Files .env>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn pretty-quick --staged
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in('app')
|
||||
->in('database')
|
||||
->in('routes')
|
||||
->in('tests');
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
return $config->setRules([
|
||||
'@Symfony' => true,
|
||||
'align_multiline_comment' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'increment_style' => ['style' => 'post'],
|
||||
'list_syntax' => ['syntax' => 'short'],
|
||||
'yoda_style' => false,
|
||||
'global_namespace_import' => [
|
||||
'import_constants' => true,
|
||||
'import_functions' => true,
|
||||
'import_classes' => null,
|
||||
],
|
||||
])
|
||||
->setFinder($finder);
|
||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
34
.vscode/launch.json
vendored
34
.vscode/launch.json
vendored
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Jest Tests",
|
||||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||
"args": ["${file}"],
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
},
|
||||
{
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"name": "Launch with XDebug",
|
||||
"ignore": ["**/vendor/**/*.php"]
|
||||
},
|
||||
{
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"name": "Launch with Firefox Debugger",
|
||||
"url": "http://localhost/",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"pathMappings": [
|
||||
{
|
||||
"url": "webpack:///",
|
||||
"path": "${workspaceFolder}/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
82
Dockerfile
82
Dockerfile
|
|
@ -1,82 +0,0 @@
|
|||
FROM composer:latest as vendor
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
RUN composer install \
|
||||
--prefer-dist \
|
||||
--no-dev \
|
||||
--no-suggest \
|
||||
--no-progress \
|
||||
--no-autoloader \
|
||||
--no-scripts \
|
||||
--no-interaction \
|
||||
--ignore-platform-reqs
|
||||
|
||||
FROM node:alpine as frontend
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY postcss.config.js tsconfig.build.json tsconfig.json webpack.config.ts ./
|
||||
COPY tools/*Plugin.ts ./tools/
|
||||
|
||||
COPY resources ./resources
|
||||
|
||||
RUN yarn build && \
|
||||
cp resources/assets/src/images/bg.webp public/app/ && \
|
||||
cp resources/assets/src/images/favicon.ico public/app/ && \
|
||||
# Strip unused files
|
||||
rm -rf *.config.js *.config.ts tsconfig.* \
|
||||
package.json yarn.lock node_modules/ \
|
||||
resources/assets/ resources/lang resources/misc resources/misc/backgrounds/ \
|
||||
tools/
|
||||
|
||||
FROM composer:latest as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
|
||||
COPY --from=vendor /app ./
|
||||
COPY --from=frontend /app/public ./public
|
||||
COPY --from=frontend /app/resources/views/assets ./resources/views/assets
|
||||
|
||||
RUN composer dump-autoload -o --no-dev -n && \
|
||||
rm -rf *.config.js *.config.ts tsconfig.* \
|
||||
package.json yarn.lock node_modules/ \
|
||||
resources/assets/ resources/misc resources/misc/backgrounds/ \
|
||||
tools/ && \
|
||||
mv .env.example .env && \
|
||||
php artisan key:generate && \
|
||||
mv .env storage/ && \
|
||||
ln -s storage/.env .env && \
|
||||
touch storage/database.db && \
|
||||
mkdir storage/plugins && \
|
||||
sed 's/PLUGINS_DIR=null/PLUGINS_DIR=\/app\/storage\/plugins/' -i storage/.env && \
|
||||
sed 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' -i storage/.env && \
|
||||
sed 's/DB_DATABASE=blessingskin/DB_DATABASE=\/app\/storage\/database\.db/' -i storage/.env
|
||||
|
||||
FROM php:8-apache
|
||||
|
||||
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
RUN chmod +x /usr/local/bin/install-php-extensions && \
|
||||
install-php-extensions gd zip
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app ./
|
||||
|
||||
ENV APACHE_DOCUMENT_ROOT /app/public
|
||||
RUN chown -R www-data:www-data . && \
|
||||
sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf && \
|
||||
sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf && \
|
||||
a2enmod rewrite headers
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
VOLUME ["/app/storage"]
|
||||
687
LICENSE
687
LICENSE
|
|
@ -1,21 +1,674 @@
|
|||
MIT License
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (c) 2016-present The Blessing Skin Team
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
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:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
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.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{one line to give the program's name and a brief idea of what it does.}
|
||||
Copyright (C) {year} {name of author}
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
{project} Copyright (C) {year} {fullname}
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
|
|
|||
92
README-zh.md
92
README-zh.md
|
|
@ -1,92 +0,0 @@
|
|||
- **简体中文**
|
||||
- [English](./README.md)
|
||||
|
||||
<p align="center"><img src="https://media.githubusercontent.com/media/bs-community/logo/main/logo.png"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/bs-community/blessing-skin-server/CI?style=flat-square"></a>
|
||||
<a href="https://codecov.io/gh/bs-community/blessing-skin-server"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/bs-community/blessing-skin-server?style=flat-square"></a>
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/releases"><img alt="GitHub release (latest SemVer including pre-releases)" src="https://img.shields.io/github/v/release/bs-community/blessing-skin-server?include_prereleases&style=flat-square"></a>
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/blob/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/bs-community/blessing-skin-server?style=flat-square"></a>
|
||||
<a href="https://discord.com/invite/QAsyEyt"><img alt="Discord" src="https://discord.com/api/guilds/761226550921658380/widget.png"></a>
|
||||
</p>
|
||||
|
||||
优雅的开源 Minecraft 皮肤站,现在,回应您的等待。
|
||||
|
||||
Blessing Skin 是一款能让您上传、管理和分享您的 Minecraft 皮肤和披风的 Web 应用程序。与修改游戏材质包不同的是,所有人都能在游戏中看到各自的皮肤和披风(当然,前提是玩家们要使用同一个皮肤站)。
|
||||
|
||||
Blessing Skin 是一个开源的 PHP 项目,这意味着您可以自由地在您的服务器上部署它。
|
||||
|
||||
## 特性
|
||||
|
||||
- 完整实现了一个皮肤站该有的功能
|
||||
- 支持单用户多个角色
|
||||
- 通过皮肤库来分享您的皮肤和披风!
|
||||
- 易于使用
|
||||
- 可视化的用户、角色、材质管理页面
|
||||
- 详细的站点配置页面
|
||||
- 多处 UI/UX 优化只为更好的用户体验
|
||||
- 安全
|
||||
- 支持多种安全密码 Hash 算法
|
||||
- 注册可要求 Email 验证
|
||||
- 防止恶意请求的积分系统
|
||||
- 强大的可扩展性
|
||||
- 多种多样的插件
|
||||
- 支持与 Authme/Discuz 等程序的用户数据对接(插件)
|
||||
- 支持自定义 Yggdrasil API 外置登录系统(插件)
|
||||
|
||||
## 环境要求
|
||||
|
||||
Blessing Skin 对您的服务器有一定的要求。在大多数情况下,下列所需的 PHP 扩展已经开启。
|
||||
|
||||
- 一台支持 URL 重写的主机,Nginx 或 Apache
|
||||
- PHP >= 8.1.0
|
||||
- 安装并启用如下 PHP 扩展:
|
||||
- OpenSSL >= 1.1.1 (TLS 1.3)
|
||||
- PDO
|
||||
- Mbstring
|
||||
- Tokenizer
|
||||
- GD
|
||||
- XML
|
||||
- Ctype
|
||||
- JSON
|
||||
- fileinfo
|
||||
- zip
|
||||
- Imagick
|
||||
|
||||
## 快速使用
|
||||
|
||||
请参阅 [安装指南](https://blessing.netlify.app/setup.html)。
|
||||
|
||||
## 插件系统
|
||||
|
||||
Blessing Skin 提供了强大的插件系统,您可以通过添加多种多样的插件来为您的皮肤站添加功能。
|
||||
|
||||
## 自行构建
|
||||
|
||||
详情可阅读 [这里](https://blessing.netlify.app/build.html)。
|
||||
|
||||
> 您可以订阅我们的 Telegram 频道 [Blessing Skin News](https://t.me/blessing_skin_news) 来获取最新开发动态。当有新的 Commit 被推送时,我们的机器人将会在频道内发送一条消息来提示您能否拉取最新代码,以及拉取后应该做什么。
|
||||
|
||||
## 国际化(i18n)
|
||||
|
||||
Blessing Skin 可支持多种语言,当前支持英语、简体中文和西班牙语。
|
||||
|
||||
如果您愿意将您的翻译贡献出来,欢迎参与 [我们的 Crowdin 项目](https://crowdin.com/project/blessing-skin)。
|
||||
|
||||
## 问题报告
|
||||
|
||||
请参阅 [报告问题的正确姿势](https://blessing.netlify.app/report.html)。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [用户手册](https://blessing.netlify.app/)
|
||||
- [插件开发文档](https://bs-plugin.netlify.app/)
|
||||
|
||||
## 版权
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-present The Blessing Skin Team
|
||||
|
||||
程序原作者为 [@printempw](https://printempw.github.io/),转载请注明。
|
||||
311
README.md
311
README.md
|
|
@ -1,90 +1,275 @@
|
|||
- [简体中文](./README-zh.md)
|
||||
- **English**
|
||||
# Blessing Skin Server
|
||||
|
||||
<p align="center"><img src="https://media.githubusercontent.com/media/bs-community/logo/main/logo.png"></p>
|
||||
优雅的开源 PHP Minecraft 皮肤站,现已更新至 v3。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/bs-community/blessing-skin-server/CI.yml?branch=dev&style=flat-square"></a>
|
||||
<a href="https://codecov.io/gh/bs-community/blessing-skin-server"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/bs-community/blessing-skin-server?style=flat-square"></a>
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/releases"><img alt="GitHub release (latest SemVer including pre-releases)" src="https://img.shields.io/github/v/release/bs-community/blessing-skin-server?include_prereleases&style=flat-square"></a>
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/blob/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/bs-community/blessing-skin-server?style=flat-square"></a>
|
||||
<a href="https://discord.com/invite/QAsyEyt"><img alt="Discord" src="https://discord.com/api/guilds/761226550921658380/widget.png"></a>
|
||||
</p>
|
||||

|
||||
|
||||
Puzzled by losing your custom skins in Minecraft servers runing in offline mode? Now you can easily get them back with the help of Blessing Skin!
|
||||
特性:
|
||||
-----------
|
||||
- 支持 [UniSkinAPI](https://github.com/RecursiveG/UniSkinServer/blob/master/doc/UniSkinAPI_zh-CN.md)
|
||||
- 支持 [CustomSkinLoader API](https://github.com/xfl03/CustomSkinLoaderAPI/blob/master/CustomSkinAPI/CustomSkinAPI_en.md)
|
||||
- 同时支持旧版样式链接
|
||||
- ~~支持与 Authme、CrazyLogin、Discuz 等程序进行数据对接~~ V3 的数据对接还在开发中
|
||||
- 支持一个用户多个角色
|
||||
- 皮肤库、衣柜功能
|
||||
- 积分系统,防止用户恶意上传/添加角色
|
||||
- 完善的用户管理后台以及配置页面
|
||||
- 多种后台配色
|
||||
- 可以获取由皮肤生成的头像
|
||||
|
||||
Blessing Skin is a web application where you can upload, manage and share your custom skins & capes! Unlike modifying a resource pack, everyone in the game will see the different skins of each other (of course they should register at the same website too).
|
||||
面向开发者们的特性:
|
||||
-----------
|
||||
- MVC 设计模式,使用强大的 Blade 模板引擎和 Eloquent ORM
|
||||
- 使用 composer、bower 等包管理器管理依赖
|
||||
- 几乎所有请求都使用 ajax 发送
|
||||
- 使用 CSS 预处理器 Sass
|
||||
- 使用 gulp 作为前端构建工具
|
||||
|
||||
Blessing Skin is an open-source project written in PHP, which means you can deploy it freely on your own web server!
|
||||
|
||||
## Features
|
||||
环境要求:
|
||||
-----------
|
||||
1. 一台支持 URL 重写的主机,Nginx、Apache 或 IIS
|
||||
2. **PHP 版本 >= 5.5.9**
|
||||
3. PHP 安装 GD 扩展库
|
||||
4. 目录的写权限
|
||||
5. 不支持安装在子目录
|
||||
|
||||
- A fully functional skin hosting service
|
||||
- Multiple player names can be owned by one user on the website
|
||||
- Share your skins and capes online with skin library!
|
||||
- Easy-to-use
|
||||
- Visual page for user/player/texture management
|
||||
- Detailed option pages
|
||||
- Many tweaks for a better UI/UX
|
||||
- Security
|
||||
- Support many secure password hash algorithms
|
||||
- Email verification for registration
|
||||
- Score system for preventing evil requests
|
||||
- Incredibly extensible
|
||||
- Plenty of plugins available
|
||||
- Integration with Authme/Discuz (available as plugin)
|
||||
- Support custom Yggdrasil API authentication (available as plugin)
|
||||
快速使用:
|
||||
-----------
|
||||
1. 下载发布的打包版源码,重命名 `.env.example` 为 `.env` 并配置你的数据库连接信息(如果是 windows 就重命名为 `.env.`,后面那个点会自动去掉的)
|
||||
2. 访问 `/setup/index.php` 进行安装
|
||||
3. 如果你是用的是 Nginx,请配置你的 `nginx.conf` 并加入重写规则
|
||||
4. 访问你的站点,注册一个新账户或者使用 `安装时所配置的账户` 登录
|
||||
5. (在数据库的 `users` 表中将你的用户 permission 字段设置为 `1` 即可获取管理员权限, 设置为 `2` 即为超级管理员)
|
||||
6. 在角色管理面板使用你的 Minecraft 角色名添加一个新角色
|
||||
7. 在皮肤库上传你的皮肤 & 披风(可设为私有)并添加至衣柜
|
||||
8. 应用皮肤 & 披风到你的角色
|
||||
9. 在你所使用的皮肤 Mod 配置文件中加入你的地址
|
||||
10. 完成啦~
|
||||
|
||||
## Requirements
|
||||
自行构建:
|
||||
------------
|
||||
普通用户下载打包版并按照 `快速使用` 中的方法配置即可,但是如果你想要自定义 Blessing Skin Server 的一些内容的话,就需要自己用源码构建啦。
|
||||
|
||||
Blessing Skin has only a few system requirements. In most cases, these PHP extensions are already enabled.
|
||||
**不推荐不熟悉 shell 操作以及不想折腾的用户使用。**
|
||||
|
||||
- Web server with URL rewriting enabled (Nginx or Apache)
|
||||
- PHP >= 8.1.0
|
||||
- PHP Extensions
|
||||
- OpenSSL >= 1.1.1 (TLS 1.3)
|
||||
- PDO
|
||||
- Mbstring
|
||||
- Tokenizer
|
||||
- GD
|
||||
- XML
|
||||
- Ctype
|
||||
- JSON
|
||||
- fileinfo
|
||||
- zip
|
||||
- Imagick
|
||||
先从 git 上 clone 源码:
|
||||
|
||||
## Quick Install
|
||||
```
|
||||
$ git clone https://github.com/printempw/blessing-skin-server.git
|
||||
```
|
||||
|
||||
Please read [Installation Guide](https://blessing.netlify.app/en/setup.html).
|
||||
使用 composer 安装 PHP 依赖:
|
||||
|
||||
## Plugin System
|
||||
```
|
||||
$ composer install
|
||||
```
|
||||
|
||||
Blessing Skin provides an elegant and powerful plugin system, and you can attach plenty of functions and customization to your site via installing plugins.
|
||||
使用 bower 安装前端依赖库:
|
||||
|
||||
## Build From Source
|
||||
```
|
||||
$ bower install
|
||||
```
|
||||
|
||||
Please refer to [Manual Build](https://blessing.netlify.app/build.html).
|
||||
使用 gulp 构建前端代码:
|
||||
|
||||
## Internationalization
|
||||
```
|
||||
$ gulp copy && gulp build
|
||||
```
|
||||
|
||||
Blessing Skin supports multiple languages, while currently supporting English, Simplified Chinese and Spanish.
|
||||
可以开始使用啦~
|
||||
|
||||
If you are willing to contribute your translation, welcome to join [our Crowdin project](https://crowdin.com/project/blessing-skin).
|
||||
服务器配置:
|
||||
------------
|
||||
如果你使用 Apache 或者 IIS 作为 web 服务器(大部分的虚拟主机),那么恭喜你,我已经帮你把重写规则写好啦,开箱即用,无需任何配置~
|
||||
|
||||
## Report Bugs
|
||||
如果你使用 Nginx,请在你的 `nginx.conf` 中加入如下规则**(重要)**:
|
||||
|
||||
Read [FAQ](https://blessing.netlify.app/faq.html) and double check if your situation doesn't suit any case mentioned there before reporting.
|
||||
```
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
When reporting a problem, please attach your log file (located at `storage/logs/laravel.log`) and the information of your server where the error occured on. You should also read this [guide](https://blessing.netlify.app/report.html) before reporting a problem.
|
||||
# Protect .env file
|
||||
location ~ /\.env
|
||||
{
|
||||
deny all;
|
||||
}
|
||||
```
|
||||
|
||||
## Related Links
|
||||
现在你可以访问 `http://example.com/{ player_name }.json` 来得到你的首选 API(可在后台配置)的 JSON 用户数据。另外一个 API 的 JSON 数据可以通过访问 `http://example.com/(usm|csl)/{ player_name }.json` 得到。
|
||||
|
||||
- [User Manual](https://blessing.netlify.app/en/)
|
||||
- [Plugins Development Documentation](https://bs-plugin.netlify.app/)
|
||||
上传完皮肤后,你就可以访问 `http://example.com/skin/{ player_name }.png` 得到你的首选模型皮肤啦。 披风图片在这里:`http://example.com/cape/{ player_name }.png` 。
|
||||
|
||||
## Copyright & License
|
||||
客户端配置:
|
||||
------------
|
||||
#### CustomSkinLoader 13.1 及以上(推荐)
|
||||
|
||||
MIT License
|
||||
CustomSkinLoader 13.1 经过作者的完全重写,支持了 CSL API,并且使用了高端洋气的 JSON 配置文件。你问我 JSON 是什么?为什么不去问问神奇海螺呢。
|
||||
|
||||
Copyright (c) 2016-present The Blessing Skin Team
|
||||
配置文件位于 `.minecraft/CustomSkinLoader/CustomSkinLoader.json`,你需要在 loadlist 数组最顶端加入你的皮肤站配置。
|
||||
|
||||
举个栗子(原来的 JSON 长这样):
|
||||
|
||||
```json
|
||||
{
|
||||
"enable": true,
|
||||
"loadlist": [
|
||||
{
|
||||
"name": "Mojang",
|
||||
"type": "MojangAPI"
|
||||
},
|
||||
{
|
||||
"name": "SkinMe",
|
||||
"type": "UniSkinAPI",
|
||||
"root": "http://www.skinme.cc/uniskin/"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
你需要将其修改成像这样:
|
||||
|
||||
```json
|
||||
{
|
||||
"enable": true,
|
||||
"loadlist": [
|
||||
{
|
||||
"name": "YourSkinServer",
|
||||
"type": "CustomSkinAPI",
|
||||
"root": "http://example.com/"
|
||||
},
|
||||
{
|
||||
"name": "Mojang",
|
||||
"type": "MojangAPI"
|
||||
},
|
||||
{
|
||||
"name": "SkinMe",
|
||||
"type": "UniSkinAPI",
|
||||
"root": "http://www.skinme.cc/uniskin/"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`"type"` 字段按照你的后台中配置的首选 API 来填(CustomSkinAPI|UniSkinAPI),CSL 13.1 版是支持三种加载方式的~~万受♂之王~~
|
||||
|
||||
如果还是不会填的话,请查看 CSL 开发者的 [MCBBS 发布贴](http://www.mcbbs.net/thread-269807-1-1.html)。
|
||||
|
||||
#### CustomSkinLoader 13.1 版以下:
|
||||
|
||||
在 `.minecraft/CustomSkinLoader/skinurls.txt` 中添加你的皮肤站地址:
|
||||
|
||||
```
|
||||
http://example.com/skin/*.png
|
||||
http://skins.minecraft.net/MinecraftSkins/*.png
|
||||
http://minecrack.fr.nf/mc/skinsminecrackd/*.png
|
||||
http://www.skinme.cc/MinecraftSkins/*.png
|
||||
```
|
||||
|
||||
注意你需要将你的皮肤站地址放在配置文件最上方以优先加载。
|
||||
|
||||
同理在 `.minecraft/CustomSkinLoader/capeurls.txt` 中加入:
|
||||
|
||||
```
|
||||
http://example.com/cape/*.png
|
||||
```
|
||||
|
||||
#### UniSkinMod 1.4 版及以上(推荐)
|
||||
|
||||
配置文件位于 `.minecraft/config/UniSkinMod/UniSkinMod.json`。
|
||||
|
||||
举个栗子(原来的 JSON 长这样):
|
||||
|
||||
```json
|
||||
{
|
||||
"rootURIs": [
|
||||
"http://www.skinme.cc/uniskin",
|
||||
"https://skin.prinzeugen.net"
|
||||
],
|
||||
"legacySkinURIs": [],
|
||||
"legacyCapeURIs": []
|
||||
}
|
||||
```
|
||||
|
||||
你需要在 `rootURIs` 字典中加入你的皮肤站的地址:
|
||||
|
||||
```json
|
||||
{
|
||||
"rootURIs": [
|
||||
"http://www.skinme.cc/uniskin",
|
||||
"https://skin.prinzeugen.net",
|
||||
"http://example.com"
|
||||
],
|
||||
"legacySkinURIs": [],
|
||||
"legacyCapeURIs": []
|
||||
}
|
||||
```
|
||||
|
||||
如果你的皮肤站首选 API 为 CustomSkinLoader API 的话,你需要在 UniSkinMod 配置文件中填入类似于 `http://example.com/usm` (添加后缀)来支持 UniSkinMod。
|
||||
|
||||
配置 `rootURIs` 后,`legacySkinURIs` 和 `legacyCapeURIs` 可以不用配置。详见[文档](https://github.com/RecursiveG/UniSkinMod/blob/1.9/README.md)。
|
||||
|
||||
#### UniSkinMod 1.2 及 1.3 版
|
||||
|
||||
在你 MC 客户端的 `.minecraft/config/UniSkinMod.cfg` 中加入你的皮肤站根地址:
|
||||
|
||||
举个栗子:
|
||||
|
||||
```
|
||||
# SkinMe Default
|
||||
Root: http://www.skinme.cc/uniskin
|
||||
# Your Server
|
||||
Root: http://example.com
|
||||
```
|
||||
|
||||
如果你把皮肤站安装到子目录的话,请一起带上你的子目录。
|
||||
|
||||
#### UniSkinMod 1.2 版以下
|
||||
|
||||
同样是在 `.minecraft/config/UniSkinMod.cfg` 中配置你的皮肤站地址,但是稍有点不一样。旧版的 UniSkinMod 是不支持 Json API 的,而是使用了传统图片链接的方式(其实这样的话皮肤站也好实现):
|
||||
|
||||
举个栗子:
|
||||
|
||||
```
|
||||
Skin: http://skins.minecraft.net/MinecraftSkins/%s.png
|
||||
Cape: http://skins.minecraft.net/MinecraftCloaks/%s.png
|
||||
# Your Server
|
||||
Skin: http://example.com/skin/%s.png
|
||||
Cape: http://example.com/cape/%s.png
|
||||
```
|
||||
|
||||
这是通过 URL 重写(伪静态)实现的,所以皮肤站目录下没有 `skin` 和 `cape` 目录也不要惊讶哦。
|
||||
|
||||
如果一切都正常工作,你就可以在游戏中看到你的皮肤啦~
|
||||
|
||||
> 顺带一提用户中心有一个自动生成配置的功能哦~
|
||||
|
||||

|
||||
|
||||
常见问题:
|
||||
------------
|
||||
|
||||
#### 访问 `example.com/skin/xxx.png ` 404?
|
||||
|
||||
请确认你的伪静态(URL 重写)是否配置正确。
|
||||
|
||||
#### 500 错误?
|
||||
|
||||
本程序使用了一些 PHP 5.4 的新特性,请确保你的 PHP 版本 >= 5.4
|
||||
|
||||
#### 游戏中皮肤不显示?
|
||||
|
||||
请先确认你的皮肤站 URL 重写规则已经配置正确,并且可以正常获取皮肤图片。
|
||||
|
||||
如果还是不能显示皮肤,请阅读您所使用的皮肤 Mod 的 FAQ。
|
||||
|
||||
还是不行的话,请在启动器开启调试模式,并且查看所有关于 skin 的日志, CSL 的日志位于 `.minecraft/CustomSkinLoader/CustomSkinLoader.log`。
|
||||
|
||||
一般来说看了就可以明白了,如果还是不明白请邮件 [联系我](mailto:h@prinzeugen.net)(带上你的日志)。
|
||||
|
||||
版权:
|
||||
------------
|
||||
Blessing Skin Server 程序是基于 GUN General Public License v3.0 开放源代码的自由软件,你可以遵照 GPLv3 协议来修改和重新发布这一程序。
|
||||
|
||||
程序原作者为 [@printempw](https://prinzeugen.net/),转载请注明。
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
|
||||
class BsInstallCommand extends Command
|
||||
{
|
||||
protected $signature = 'bs:install {email} {password} {nickname}';
|
||||
|
||||
protected $description = 'Execute installation and create a super administrator.';
|
||||
|
||||
public function handle(Filesystem $filesystem)
|
||||
{
|
||||
if ($filesystem->exists(storage_path('install.lock'))) {
|
||||
$this->info('You have installed Blessing Skin Server. Nothing to do.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->call('migrate', ['--force' => true]);
|
||||
if (!$this->getLaravel()->runningUnitTests()) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->call('key:generate');
|
||||
$this->call('passport:keys', ['--no-interaction' => true]);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
option(['site_url' => url('/')]);
|
||||
|
||||
$admin = new User();
|
||||
$admin->email = $this->argument('email');
|
||||
$admin->nickname = $this->argument('nickname');
|
||||
$admin->score = option('user_initial_score');
|
||||
$admin->avatar = 0;
|
||||
$admin->password = app('cipher')->hash($this->argument('password'), config('secure.salt'));
|
||||
$admin->ip = '127.0.0.1';
|
||||
$admin->permission = User::SUPER_ADMIN;
|
||||
$admin->register_at = Carbon::now();
|
||||
$admin->last_sign_at = Carbon::now()->subDay();
|
||||
$admin->verified = true;
|
||||
$admin->save();
|
||||
|
||||
$filesystem->put(storage_path('install.lock'), '');
|
||||
|
||||
$this->info('Installation completed!');
|
||||
$this->info('We recommend to modify your "Site URL" option if incorrect.');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Option;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
class OptionsCacheCommand extends Command
|
||||
{
|
||||
protected $signature = 'options:cache';
|
||||
|
||||
protected $description = 'Cache Blessing Skin options';
|
||||
|
||||
public function handle(Filesystem $filesystem, Application $app)
|
||||
{
|
||||
$path = storage_path('options.php');
|
||||
$filesystem->delete($path);
|
||||
$app->forgetInstance(Option::class);
|
||||
|
||||
$content = var_export(resolve(Option::class)->all(), true);
|
||||
$notice = '// This is auto-generated. DO NOT edit manually.'.PHP_EOL;
|
||||
$content = '<?php'.PHP_EOL.$notice.'return '.$content.';';
|
||||
$filesystem->put($path, $content);
|
||||
$this->info('Options cached successfully.');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\PluginManager;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PluginDisableCommand extends Command
|
||||
{
|
||||
protected $signature = 'plugin:disable {name}';
|
||||
|
||||
protected $description = 'Disable a plugin';
|
||||
|
||||
public function handle(PluginManager $plugins)
|
||||
{
|
||||
$plugin = $plugins->get($this->argument('name'));
|
||||
if ($plugin) {
|
||||
$plugins->disable($this->argument('name'));
|
||||
$title = trans($plugin->title);
|
||||
$this->info(trans('admin.plugins.operations.disabled', ['plugin' => $title]));
|
||||
} else {
|
||||
$this->warn(trans('admin.plugins.operations.not-found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\PluginManager;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PluginEnableCommand extends Command
|
||||
{
|
||||
protected $signature = 'plugin:enable {name}';
|
||||
|
||||
protected $description = 'Enable a plugin';
|
||||
|
||||
public function handle(PluginManager $plugins)
|
||||
{
|
||||
$name = $this->argument('name');
|
||||
$result = $plugins->enable($name);
|
||||
if ($result === true) {
|
||||
$plugin = $plugins->get($name);
|
||||
$title = trans($plugin->title);
|
||||
$this->info(trans('admin.plugins.operations.enabled', ['plugin' => $title]));
|
||||
} elseif ($result === false) {
|
||||
$this->warn(trans('admin.plugins.operations.not-found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SaltRandomCommand extends Command
|
||||
{
|
||||
protected $signature = 'salt:random {--show : Display the salt instead of modifying files}';
|
||||
|
||||
protected $description = 'Set the application salt';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$salt = $this->generateRandomSalt();
|
||||
|
||||
if ($this->option('show')) {
|
||||
return $this->line('<comment>'.$salt.'</comment>');
|
||||
}
|
||||
|
||||
// Next, we will replace the application salt in the environment file so it is
|
||||
// automatically setup for this developer. This salt gets generated using a
|
||||
// secure random byte generator and is later base64 encoded for storage.
|
||||
$this->setKeyInEnvironmentFile($salt);
|
||||
|
||||
$this->laravel['config']['secure.salt'] = $salt;
|
||||
|
||||
$this->info("Application salt [$salt] set successfully.");
|
||||
}
|
||||
|
||||
protected function setKeyInEnvironmentFile(string $salt)
|
||||
{
|
||||
file_put_contents($this->laravel->environmentFilePath(), str_replace(
|
||||
'SALT = '.$this->laravel['config']['secure.salt'],
|
||||
'SALT = '.$salt,
|
||||
file_get_contents($this->laravel->environmentFilePath())
|
||||
));
|
||||
}
|
||||
|
||||
protected function generateRandomSalt(): string
|
||||
{
|
||||
return bin2hex(resolve(\Illuminate\Contracts\Encryption\Encrypter::class)->generateKey('AES-128-CBC'));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Composer\Semver\Comparator;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel as Artisan;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class UpdateCommand extends Command
|
||||
{
|
||||
protected $signature = 'update';
|
||||
|
||||
protected $description = 'Execute update.';
|
||||
|
||||
public function handle(Artisan $artisan, Filesystem $filesystem)
|
||||
{
|
||||
$this->procedures()->each(function ($procedure, $version) {
|
||||
if (Comparator::lessThan(option('version'), $version)) {
|
||||
$procedure();
|
||||
}
|
||||
});
|
||||
|
||||
option(['version' => config('app.version')]);
|
||||
$artisan->call('migrate', ['--force' => true]);
|
||||
$artisan->call('view:clear');
|
||||
$filesystem->put(storage_path('install.lock'), '');
|
||||
Cache::flush();
|
||||
|
||||
$this->info(trans('setup.updates.success.title'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function procedures()
|
||||
{
|
||||
return collect([
|
||||
// this is just for testing
|
||||
'0.0.1' => fn () => event('__0.0.1'),
|
||||
'5.0.0' => function () {
|
||||
if (option('home_pic_url') === './app/bg.jpg') {
|
||||
option(['home_pic_url' => './app/bg.webp']);
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
protected $commands = [
|
||||
\Laravel\Passport\Console\KeysCommand::class,
|
||||
Commands\BsInstallCommand::class,
|
||||
Commands\OptionsCacheCommand::class,
|
||||
Commands\PluginDisableCommand::class,
|
||||
Commands\PluginEnableCommand::class,
|
||||
Commands\SaltRandomCommand::class,
|
||||
Commands\UpdateCommand::class,
|
||||
];
|
||||
}
|
||||
269
app/Controllers/AdminController.php
Normal file
269
app/Controllers/AdminController.php
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerModel;
|
||||
use App\Models\Texture;
|
||||
use App\Exceptions\E;
|
||||
use Validate;
|
||||
use Utils;
|
||||
use View;
|
||||
|
||||
class AdminController extends BaseController
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
View::show('admin.index');
|
||||
}
|
||||
|
||||
public function customize()
|
||||
{
|
||||
View::show('admin.customize');
|
||||
}
|
||||
|
||||
public function score()
|
||||
{
|
||||
View::show('admin.score');
|
||||
}
|
||||
|
||||
public function options()
|
||||
{
|
||||
View::show('admin.options');
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$action = Utils::getValue('action', $_GET);
|
||||
|
||||
if ($action == "check") {
|
||||
$updater = new \Updater(\App::version());
|
||||
if ($updater->newVersionAvailable()) {
|
||||
View::json([
|
||||
'new_version_available' => true,
|
||||
'latest_version' => $updater->latest_version
|
||||
]);
|
||||
} else {
|
||||
View::json([
|
||||
'new_version_available' => false,
|
||||
'latest_version' => $updater->current_version
|
||||
]);
|
||||
}
|
||||
} elseif ($action == "download") {
|
||||
View::show('admin.download');
|
||||
} else {
|
||||
View::show('admin.update');
|
||||
}
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
$page = isset($_GET['page']) ? $_GET['page'] : 1;
|
||||
|
||||
$filter = isset($_GET['filter']) ? $_GET['filter'] : "";
|
||||
|
||||
$q = isset($_GET['q']) ? $_GET['q'] : "";
|
||||
|
||||
if ($filter == "") {
|
||||
$users = UserModel::orderBy('uid');
|
||||
} elseif ($filter == "email") {
|
||||
$users = UserModel::like('email', $q)->orderBy('uid');
|
||||
} elseif ($filter == "nickname") {
|
||||
$users = UserModel::like('nickname', $q)->orderBy('uid');
|
||||
}
|
||||
|
||||
$total_pages = ceil($users->count() / 30);
|
||||
$users = $users->skip(($page - 1) * 30)->take(30)->get();
|
||||
|
||||
echo View::make('admin.users')->with('users', $users)
|
||||
->with('filter', $filter)
|
||||
->with('q', $q)
|
||||
->with('page', $page)
|
||||
->with('total_pages', $total_pages)
|
||||
->render();
|
||||
}
|
||||
|
||||
public function players()
|
||||
{
|
||||
$page = isset($_GET['page']) ? $_GET['page'] : 1;
|
||||
|
||||
$filter = isset($_GET['filter']) ? $_GET['filter'] : "";
|
||||
|
||||
$q = isset($_GET['q']) ? $_GET['q'] : "";
|
||||
|
||||
if ($filter == "") {
|
||||
$players = PlayerModel::orderBy('uid');
|
||||
} elseif ($filter == "player_name") {
|
||||
$players = PlayerModel::like('player_name', $q)->orderBy('uid');
|
||||
} elseif ($filter == "uid") {
|
||||
$players = PlayerModel::where('uid', $q)->orderBy('uid');
|
||||
}
|
||||
|
||||
$total_pages = ceil($players->count() / 30);
|
||||
$players = $players->skip(($page - 1) * 30)->take(30)->get();
|
||||
|
||||
echo View::make('admin.players')->with('players', $players)
|
||||
->with('filter', $filter)
|
||||
->with('q', $q)
|
||||
->with('page', $page)
|
||||
->with('total_pages', $total_pages)
|
||||
->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ajax request from /admin/users
|
||||
*/
|
||||
public function userAjaxHandler()
|
||||
{
|
||||
$action = isset($_GET['action']) ? $_GET['action'] : "";
|
||||
|
||||
if ($action == "color") {
|
||||
Validate::checkPost(['color_scheme']);
|
||||
|
||||
$color_scheme = str_replace('_', '-', $_POST['color_scheme']);
|
||||
\Option::set('color_scheme', $color_scheme);
|
||||
|
||||
View::json('修改配色成功', 0);
|
||||
}
|
||||
|
||||
$user = new User(Utils::getValue('uid', $_POST));
|
||||
// current user
|
||||
$cur_user = new User($_SESSION['uid']);
|
||||
|
||||
if (!$user->is_registered)
|
||||
throw new E('用户不存在', 1);
|
||||
|
||||
if ($action == "email") {
|
||||
Validate::checkPost(['email']);
|
||||
|
||||
if (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
View::json('邮箱格式错误', 3);
|
||||
}
|
||||
|
||||
if ($user->setEmail($_POST['email']))
|
||||
View::json('邮箱修改成功', 0);
|
||||
|
||||
} elseif ($action == "nickname") {
|
||||
Validate::checkPost(['nickname']);
|
||||
|
||||
if (Utils::convertString($_POST['nickname']) != $_POST['nickname'])
|
||||
View::json('无效的昵称。昵称中包含了奇怪的字符。', 1);
|
||||
|
||||
if ($user->setNickName($_POST['nickname']))
|
||||
View::json('昵称已成功设置为 '.$_POST['nickname'], 0);
|
||||
|
||||
} elseif ($action == "password") {
|
||||
Validate::checkPost(['password']);
|
||||
|
||||
if (\Validate::password($_POST['password'])) {
|
||||
if ($user->changePasswd($_POST['password']))
|
||||
View::json('密码修改成功', 0);
|
||||
}
|
||||
|
||||
} elseif ($action == "score") {
|
||||
Validate::checkPost(['score']);
|
||||
|
||||
if ($user->setScore($_POST['score']))
|
||||
View::json('积分修改成功', 0);
|
||||
|
||||
} elseif ($action == "ban") {
|
||||
if ($user->getPermission() == "1") {
|
||||
if ($cur_user->getPermission() != "2")
|
||||
View::json('非超级管理员无法封禁普通管理员');
|
||||
} elseif ($user->getPermission() == "2") {
|
||||
View::json('超级管理员无法被封禁');
|
||||
}
|
||||
|
||||
$permission = $user->getPermission() == "-1" ? "0" : "-1";
|
||||
|
||||
if ($user->setPermission($permission)) {
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '账号已被' . ($permission == '-1' ? '封禁' : '解封'),
|
||||
'permission' => $user->getPermission()
|
||||
]);
|
||||
}
|
||||
|
||||
} elseif ($action == "admin") {
|
||||
if ($cur_user->getPermission() != "2")
|
||||
View::json('非超级管理员无法进行此操作');
|
||||
|
||||
if ($user->getPermission() == "2")
|
||||
View::json('超级管理员无法被解除');
|
||||
|
||||
$permission = $user->getPermission() == "1" ? "0" : "1";
|
||||
|
||||
if ($user->setPermission($permission)) {
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '账号已被' . ($permission == '1' ? '设为' : '解除') . '管理员',
|
||||
'permission' => $user->getPermission()
|
||||
]);
|
||||
}
|
||||
|
||||
} elseif ($action == "delete") {
|
||||
if ($user->delete())
|
||||
View::json('账号已被成功删除', 0);
|
||||
|
||||
} else {
|
||||
throw new E('非法参数', 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ajax request from /admin/players
|
||||
*/
|
||||
public function playerAjaxHandler()
|
||||
{
|
||||
$action = isset($_GET['action']) ? $_GET['action'] : "";
|
||||
|
||||
// exception will be throw by model if player is not existent
|
||||
$player = new Player(Utils::getValue('pid', $_POST));
|
||||
|
||||
if ($action == "preference") {
|
||||
Validate::checkPost(['preference']);
|
||||
|
||||
if ($_POST['preference'] != "default" && $_POST['preference'] != "slim")
|
||||
View::json('无效的参数', 0);
|
||||
|
||||
if ($player->setPreference($_POST['preference']))
|
||||
View::json('角色 '.$player->player_name.' 的优先模型已更改至 '.$_POST['preference'], 0);
|
||||
|
||||
} elseif ($action == "texture") {
|
||||
Validate::checkPost(['model', 'tid']);
|
||||
|
||||
if ($_POST['model'] != "steve" && $_POST['model'] != "alex" && $_POST['model'] != "cape")
|
||||
View::json('无效的参数', 0);
|
||||
|
||||
if (!(is_numeric($_POST['tid']) && Texture::find($_POST['tid'])))
|
||||
View::json('材质 tid.'.$_POST['tid'].' 不存在', 1);
|
||||
|
||||
if ($player->setTexture(['tid_'.$_POST['model'] => $_POST['tid']]))
|
||||
View::json('角色 '.$player->player_name.' 的材质修改成功', 0);
|
||||
|
||||
} elseif ($action == "owner") {
|
||||
Validate::checkPost(['uid']);
|
||||
|
||||
if (!is_numeric($_POST['uid']))
|
||||
View::json('无效的参数', 0);
|
||||
|
||||
$user = new User($_POST['uid']);
|
||||
|
||||
if (!$user->is_registered)
|
||||
View::json('不存在的用户', 1);
|
||||
|
||||
if ($player->setOwner($_POST['uid']))
|
||||
View::json('角色 '.$player->player_name.' 已成功让渡至 '.$user->getNickName(), 0);
|
||||
|
||||
} elseif ($action == "delete") {
|
||||
if (PlayerModel::where('pid', $_POST['pid'])->delete())
|
||||
View::json('角色已被成功删除', 0);
|
||||
} else {
|
||||
throw new E('非法参数', 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
228
app/Controllers/AuthController.php
Normal file
228
app/Controllers/AuthController.php
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserModel;
|
||||
use App\Exceptions\E;
|
||||
use Validate;
|
||||
use Mail;
|
||||
use View;
|
||||
use Utils;
|
||||
use Option;
|
||||
use Http;
|
||||
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
public function login()
|
||||
{
|
||||
View::show('auth.login');
|
||||
}
|
||||
|
||||
public function handleLogin()
|
||||
{
|
||||
// instantiate user
|
||||
$user = ($_SESSION['auth_type'] == 'email') ?
|
||||
new User(null, ['email' => $_POST['email']]) :
|
||||
new User(null, ['username' => $_POST['username']]);
|
||||
|
||||
if (Utils::getValue('login_fails', $_SESSION) > 3) {
|
||||
if (strtolower(Utils::getValue('captcha', $_POST)) != strtolower($_SESSION['phrase']))
|
||||
View::json('验证码填写错误', 1);
|
||||
}
|
||||
|
||||
if (!$user->is_registered) {
|
||||
View::json('用户不存在哦', 2);
|
||||
} else {
|
||||
if ($user->checkPasswd($_POST['password'])) {
|
||||
unset($_SESSION['login_fails']);
|
||||
|
||||
$_SESSION['uid'] = $user->uid;
|
||||
$_SESSION['token'] = $user->getToken();
|
||||
|
||||
$time = $_POST['keep'] == true ? 86400 : 3600;
|
||||
|
||||
setcookie('uid', $user->uid, time()+$time, '/');
|
||||
setcookie('token', $user->getToken(), time()+$time, '/');
|
||||
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '登录成功,欢迎回来~',
|
||||
'token' => $user->getToken()
|
||||
]);
|
||||
} else {
|
||||
$_SESSION['login_fails'] = isset($_SESSION['login_fails']) ?
|
||||
$_SESSION['login_fails'] + 1 : 1;
|
||||
|
||||
View::json([
|
||||
'errno' => 1,
|
||||
'msg' => '邮箱或密码不对哦~',
|
||||
'login_fails' => $_SESSION['login_fails']
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
if (isset($_SESSION['token'])) {
|
||||
setcookie('uid', '', time() - 3600, '/');
|
||||
setcookie('token', '', time() - 3600, '/');
|
||||
|
||||
session_destroy();
|
||||
|
||||
View::json('登出成功~', 0);
|
||||
} else {
|
||||
throw new E('并没有有效的 session', 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
if (Option::get('user_can_register') == 1) {
|
||||
View::show('auth.register');
|
||||
} else {
|
||||
throw new E('残念。。本皮肤站已经关闭注册咯 QAQ', 7, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleRegister()
|
||||
{
|
||||
if (strtolower(Utils::getValue('captcha', $_POST)) != strtolower($_SESSION['phrase']))
|
||||
View::json('验证码填写错误', 1);
|
||||
|
||||
$user = new User(null, ['email' => $_POST['email']]);
|
||||
|
||||
if (!$user->is_registered) {
|
||||
if (Option::get('user_can_register') == 1) {
|
||||
if (Validate::password($_POST['password'])) {
|
||||
// If amount of registered accounts of IP is more than allowed amounts,
|
||||
// then reject the register.
|
||||
if (UserModel::where('ip', Http::getRealIP())->count() < Option::get('regs_per_ip'))
|
||||
{
|
||||
if (Validate::nickname(Utils::getValue('nickname', $_POST)))
|
||||
View::json('无效的昵称,昵称不能包含奇怪的字符', 1);
|
||||
|
||||
// register new user
|
||||
$user = $user->register($_POST['password'], Http::getRealIP());
|
||||
$user->setNickName($_POST['nickname']);
|
||||
|
||||
// set cookies
|
||||
setcookie('uid', $user->uid, time() + 3600, '/');
|
||||
setcookie('token', $user->getToken(), time() + 3600, '/');
|
||||
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '注册成功,正在跳转~',
|
||||
'token' => $user->getToken()
|
||||
]);
|
||||
|
||||
} else {
|
||||
View::json('你最多只能注册 '.Option::get('regs_per_ip').' 个账户哦', 7);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
View::json('残念。。本皮肤站已经关闭注册咯 QAQ', 7);
|
||||
}
|
||||
} else {
|
||||
View::json('这个邮箱已经注册过啦,换一个吧', 5);
|
||||
}
|
||||
}
|
||||
|
||||
public function forgot()
|
||||
{
|
||||
if ($_ENV['MAIL_HOST'] != "") {
|
||||
View::show('auth.forgot');
|
||||
} else {
|
||||
throw new E('本站已关闭重置密码功能', 8, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleForgot()
|
||||
{
|
||||
if (strtolower(Utils::getValue('captcha', $_POST)) != strtolower($_SESSION['phrase']))
|
||||
View::json('验证码填写错误', 1);
|
||||
|
||||
if ($_ENV['MAIL_HOST'] == "")
|
||||
View::json('本站已关闭重置密码功能', 1);
|
||||
|
||||
if (isset($_SESSION['last_mail_time']) && (time() - $_SESSION['last_mail_time']) < 60)
|
||||
View::json('你邮件发送得太频繁啦,过 60 秒后再点发送吧', 1);
|
||||
|
||||
$user = new User(null, ['email' => $_POST['email']]);
|
||||
|
||||
if (!$user->is_registered)
|
||||
View::json('该邮箱尚未注册', 1);
|
||||
|
||||
$mail = new Mail();
|
||||
|
||||
$mail->from(Option::get('site_name'))
|
||||
->to($_POST['email'])
|
||||
->subject('重置您在 '.Option::get('site_name').' 上的账户密码');
|
||||
|
||||
$uid = $user->uid;
|
||||
$token = base64_encode($user->getToken().substr(time(), 4, 6).Utils::generateRndString(16));
|
||||
|
||||
$url = Option::get('site_url')."/auth/reset?uid=$uid&token=$token";
|
||||
|
||||
$mail->content(View::make('auth.mail')->with('reset_url', $url)->render());
|
||||
|
||||
if (!$mail->send()) {
|
||||
View::json('邮件发送失败,详细信息:'.$mail->getLastError(), 2);
|
||||
} else {
|
||||
$_SESSION['last_mail_time'] = time();
|
||||
View::json('邮件已发送,一小时内有效,请注意查收.', 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if (isset($_GET['uid']) && isset($_GET['token'])) {
|
||||
$user = new User($_GET['uid']);
|
||||
if (!$user->is_registered)
|
||||
Http::redirect('./forgot', '无效的链接');
|
||||
|
||||
$token = substr(base64_decode($_GET['token']), 0, -22);
|
||||
|
||||
if ($user->getToken() != $token) {
|
||||
Http::redirect('./forgot', '无效的链接');
|
||||
}
|
||||
|
||||
$timestamp = substr(base64_decode($_GET['token']), strlen($token), 6);
|
||||
|
||||
// more than 1 hour
|
||||
if ((substr(time(), 4, 6) - $timestamp) > 3600) {
|
||||
Http::redirect('./forgot', '链接已过期');
|
||||
}
|
||||
|
||||
echo View::make('auth.reset')->with('user', $user);
|
||||
} else {
|
||||
Http::redirect('./login', '非法访问');
|
||||
}
|
||||
}
|
||||
|
||||
public function handleReset()
|
||||
{
|
||||
Validate::checkPost(['uid', 'password']);
|
||||
|
||||
if (Validate::password($_POST['password'])) {
|
||||
$user = new User($_POST['uid']);
|
||||
|
||||
$user->changePasswd($_POST['password']);
|
||||
|
||||
View::json('密码重置成功', 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function captcha()
|
||||
{
|
||||
$builder = new \Gregwar\Captcha\CaptchaBuilder;
|
||||
$builder->build($width = 100, $height = 34);
|
||||
$_SESSION['phrase'] = $builder->getPhrase();
|
||||
header('Content-type: image/jpeg');
|
||||
$builder->output();
|
||||
}
|
||||
|
||||
}
|
||||
10
app/Controllers/BaseController.php
Normal file
10
app/Controllers/BaseController.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
/**
|
||||
* 突然发现这个基类卵用没有 (;´Д`)
|
||||
*/
|
||||
class BaseController
|
||||
{
|
||||
}
|
||||
72
app/Controllers/ClosetController.php
Normal file
72
app/Controllers/ClosetController.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Texture;
|
||||
use App\Models\Closet;
|
||||
use App\Models\ClosetModel;
|
||||
use App\Exceptions\E;
|
||||
use View;
|
||||
use Option;
|
||||
|
||||
class ClosetController extends BaseController
|
||||
{
|
||||
private $closet;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->closet = new Closet($_SESSION['uid']);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$category = isset($_GET['category']) ? $_GET['category'] : "skin";
|
||||
|
||||
$page = isset($_GET['page']) ? $_GET['page'] : 1;
|
||||
|
||||
$items = array_slice($this->closet->getItems($category), ($page-1)*6, 6);
|
||||
|
||||
$total_pages = ceil(count($this->closet->getItems($category)) / 6);
|
||||
|
||||
echo View::make('user.closet')->with('items', $items)
|
||||
->with('page', $page)
|
||||
->with('category', $category)
|
||||
->with('total_pages', $total_pages)
|
||||
->with('user', (new User($_SESSION['uid'])))
|
||||
->render();
|
||||
}
|
||||
|
||||
public function info()
|
||||
{
|
||||
View::json($this->closet->getItems());
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
\Validate::checkPost(['tid', 'name']);
|
||||
|
||||
if ($this->closet->add($_POST['tid'], $_POST['name'])) {
|
||||
$t = Texture::find($_POST['tid']);
|
||||
$t->likes += 1;
|
||||
$t->save();
|
||||
|
||||
View::json('材质 '.$_POST['name'].' 收藏成功~', 0);
|
||||
}
|
||||
}
|
||||
|
||||
public function remove()
|
||||
{
|
||||
if (!is_numeric(\Utils::getValue('tid', $_POST)))
|
||||
throw new E('非法参数', 1);
|
||||
|
||||
if ($this->closet->remove($_POST['tid'])) {
|
||||
$t = Texture::find($_POST['tid']);
|
||||
$t->likes = $t->likes - 1;
|
||||
$t->save();
|
||||
|
||||
View::json('材质已从衣柜中移除', 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
30
app/Controllers/HomeController.php
Normal file
30
app/Controllers/HomeController.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class HomeController extends BaseController
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
if (isset($_COOKIE['uid']) && isset($_COOKIE['token'])) {
|
||||
$user = new User($_COOKIE['uid']);
|
||||
|
||||
if ($_COOKIE['token'] == $user->getToken() && $user->getPermission() != "-1") {
|
||||
$_SESSION['uid'] = $_COOKIE['uid'];
|
||||
$_SESSION['token'] = $_COOKIE['token'];
|
||||
} else {
|
||||
// delete cookies
|
||||
setcookie("uid", "", time() - 3600, '/');
|
||||
setcookie("token", "", time() - 3600, '/');
|
||||
}
|
||||
}
|
||||
|
||||
$user = isset($_SESSION['uid']) ? new User($_SESSION['uid']) : null;
|
||||
|
||||
echo \View::make('index')->with('user', $user);
|
||||
}
|
||||
|
||||
}
|
||||
152
app/Controllers/PlayerController.php
Normal file
152
app/Controllers/PlayerController.php
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerModel;
|
||||
use App\Models\Texture;
|
||||
use App\Exceptions\E;
|
||||
use Validate;
|
||||
use Utils;
|
||||
use Option;
|
||||
use View;
|
||||
|
||||
class PlayerController extends BaseController
|
||||
{
|
||||
private $player = null;
|
||||
|
||||
private $user = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->user = new User($_SESSION['uid']);
|
||||
|
||||
if (isset($_POST['pid'])) {
|
||||
$this->player = new Player($_POST['pid']);
|
||||
if (!$this->player)
|
||||
\Http::abort(404, '角色不存在');
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
echo View::make('user.player')->with('players', $this->user->getPlayers()->toArray())->with('user', $this->user);
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$player_name = $_POST['player_name'];
|
||||
|
||||
if (!isset($player_name))
|
||||
View::json('你还没有填写要添加的角色名哦', 1);
|
||||
|
||||
if (!Validate::playerName($player_name))
|
||||
{
|
||||
$msg = "无效的角色名。角色名只能包含" . ((Option::get('allow_chinese_playername') == "1") ? "汉字、" : "")."字母、数字以及下划线";
|
||||
View::json($msg, 2);
|
||||
}
|
||||
|
||||
if (!PlayerModel::where('player_name', $player_name)->get()->isEmpty())
|
||||
View::json('该角色名已经被其他人注册掉啦', 6);
|
||||
|
||||
if ($this->user->getScore() < Option::get('score_per_player'))
|
||||
View::json('积分不够添加角色啦', 7);
|
||||
|
||||
$player = new PlayerModel();
|
||||
$player->uid = $this->user->uid;
|
||||
$player->player_name = $player_name;
|
||||
$player->preference = "default";
|
||||
$player->last_modified = Utils::getTimeFormatted();
|
||||
$player->save();
|
||||
|
||||
$this->user->setScore(Option::get('score_per_player'), 'minus');
|
||||
|
||||
View::json('成功添加了角色 '.$player_name.'', 0);
|
||||
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$player_name = $this->player->model->player_name;
|
||||
$this->player->model->delete();
|
||||
|
||||
$this->user->setScore(Option::get('score_per_player'), 'plus');
|
||||
|
||||
View::json('角色 '.$player_name.' 已被删除', 0);
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
echo json_encode($this->player->model->toArray(), JSON_NUMERIC_CHECK);
|
||||
}
|
||||
|
||||
public function rename()
|
||||
{
|
||||
$new_player_name = Utils::getValue('new_player_name', $_POST);
|
||||
|
||||
if (!$new_player_name)
|
||||
throw new E('非法参数', 1);
|
||||
|
||||
if (!Validate::playerName($new_player_name))
|
||||
{
|
||||
$msg = "无效的角色名。角色名只能包含" . ((Option::get('allow_chinese_playername') == "1") ? "汉字、" : "")."字母、数字以及下划线";
|
||||
View::json($msg, 2);
|
||||
}
|
||||
|
||||
if (!PlayerModel::where('player_name', $new_player_name)->get()->isEmpty())
|
||||
View::json('此角色名已被他人使用,换一个吧~', 6);
|
||||
|
||||
$old_player_name = $this->player->model->player_name;
|
||||
$this->player->model->player_name = $new_player_name;
|
||||
$this->player->model->last_modified = Utils::getTimeFormatted();
|
||||
$this->player->model->save();
|
||||
|
||||
View::json('角色 '.$old_player_name.' 已更名为 '.$_POST['new_player_name'], 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper of Player::setTexture()
|
||||
*/
|
||||
public function setTexture()
|
||||
{
|
||||
$tid = Utils::getValue('tid', $_POST);
|
||||
|
||||
if (!is_numeric($tid))
|
||||
throw new E('非法参数', 1);
|
||||
|
||||
if (!($texture = Texture::find($tid)))
|
||||
View::json('Unexistent texture.', 6);
|
||||
|
||||
$field_name = "tid_".$texture->type;
|
||||
|
||||
$this->player->model->$field_name = $tid;
|
||||
$this->player->model->last_modified = Utils::getTimeFormatted();
|
||||
$this->player->model->save();
|
||||
|
||||
View::json('材质已成功应用至角色 '.$this->player->model->player_name.'', 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function clearTexture()
|
||||
{
|
||||
$this->player->clearTexture();
|
||||
|
||||
View::json('角色 '.$this->player->model->player_name.' 的材质已被成功重置', 0);
|
||||
}
|
||||
|
||||
public function setPreference()
|
||||
{
|
||||
if (!isset($_POST['preference']) ||
|
||||
($_POST['preference'] != "default" && $_POST['preference'] != "slim"))
|
||||
{
|
||||
throw new E('非法参数', 1);
|
||||
}
|
||||
|
||||
$this->player->setPreference($_POST['preference']);
|
||||
|
||||
View::json('角色 '.$this->player->player_name.' 的优先模型已更改至 '.$_POST['preference'], 0);
|
||||
}
|
||||
|
||||
}
|
||||
270
app/Controllers/SkinlibController.php
Normal file
270
app/Controllers/SkinlibController.php
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Texture;
|
||||
use App\Exceptions\E;
|
||||
use Validate;
|
||||
use Option;
|
||||
use Utils;
|
||||
use View;
|
||||
use Http;
|
||||
|
||||
class SkinlibController extends BaseController
|
||||
{
|
||||
private $user = null;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->user = isset($_SESSION['uid']) ? new User($_SESSION['uid']) : null;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$filter = isset($_GET['filter']) ? $_GET['filter'] : "skin";
|
||||
$sort = isset($_GET['sort']) ? $_GET['sort'] : "time";
|
||||
$sort_by = ($sort == "time") ? "upload_at" : $sort;
|
||||
$uid = isset($_GET['uid']) ? $_GET['uid'] : 0;
|
||||
|
||||
$page = isset($_GET['page']) ? $_GET['page'] : 1;
|
||||
|
||||
if ($filter == "skin") {
|
||||
$textures = Texture::where(function($query) {
|
||||
$query->where('type', '=', 'steve')
|
||||
->orWhere('type', '=', 'alex');
|
||||
})->orderBy($sort_by, 'desc');
|
||||
|
||||
} elseif ($filter == "user") {
|
||||
$textures = Texture::where('uploader', $uid)->orderBy($sort_by, 'desc');
|
||||
|
||||
} else {
|
||||
$textures = Texture::where('type', $filter)->orderBy($sort_by, 'desc');
|
||||
}
|
||||
|
||||
if (!is_null($this->user)) {
|
||||
// show private textures when show uploaded textures of current user
|
||||
if ($uid != $this->user->uid && !$this->user->is_admin)
|
||||
$textures = $textures->where('public', '1');
|
||||
} else {
|
||||
$textures = $textures->where('public', '1');
|
||||
}
|
||||
|
||||
$total_pages = ceil($textures->count() / 20);
|
||||
|
||||
$textures = $textures->skip(($page - 1) * 20)->take(20)->get();
|
||||
|
||||
echo View::make('skinlib.index')->with('user', $this->user)
|
||||
->with('sort', $sort)
|
||||
->with('filter', $filter)
|
||||
->with('textures', $textures)
|
||||
->with('page', $page)
|
||||
->with('total_pages', $total_pages)
|
||||
->render();
|
||||
}
|
||||
|
||||
public function search()
|
||||
{
|
||||
$q = isset($_GET['q']) ? $_GET['q'] : "";
|
||||
|
||||
$filter = isset($_GET['filter']) ? $_GET['filter'] : "skin";
|
||||
|
||||
$sort = isset($_GET['sort']) ? $_GET['sort'] : "time";
|
||||
$sort_by = ($sort == "time") ? "upload_at" : $sort;
|
||||
|
||||
if ($filter == "skin") {
|
||||
$textures = Texture::like('name', $q)->where(function($query) use ($q) {
|
||||
$query->where('public', '=', '1')
|
||||
->where('type', '=', 'steve')
|
||||
->orWhere('type', '=', 'alex');
|
||||
})->orderBy($sort_by, 'desc')->get();
|
||||
} else {
|
||||
$textures = Texture::like('name', $q)
|
||||
->where('type', $filter)
|
||||
->where('public', '1')
|
||||
->orderBy($sort_by, 'desc')->get();
|
||||
}
|
||||
|
||||
echo View::make('skinlib.search')->with('user', $this->user)
|
||||
->with('sort', $sort)
|
||||
->with('filter', $filter)
|
||||
->with('q', $q)
|
||||
->with('textures', $textures)->render();
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
if (!isset($_GET['tid'])) Http::abort(404, 'No specified tid.');
|
||||
$texture = Texture::find($_GET['tid']);
|
||||
|
||||
if (!$texture || $texture && !\Storage::exists(BASE_DIR."/textures/".$texture->hash)) {
|
||||
if (Option::get('auto_del_invalid_texture') == "1") {
|
||||
if ($texture) $texture->delete();
|
||||
Http::abort(404, '请求的材质文件已经被删除');
|
||||
}
|
||||
Http::abort(404, '请求的材质文件已经被删除,请联系管理员删除该条目');
|
||||
}
|
||||
|
||||
if ($texture->public == "0") {
|
||||
if (is_null($this->user) || ($this->user->uid != $texture->uploader && !$this->user->is_admin))
|
||||
Http::abort(404, '请求的材质已经设为隐私,仅上传者和管理员可查看');
|
||||
}
|
||||
|
||||
echo View::make('skinlib.show')->with('texture', $texture)->with('with_out_filter', true)->with('user', $this->user)->render();
|
||||
}
|
||||
|
||||
public function info($tid)
|
||||
{
|
||||
echo json_encode(Texture::find($tid)->toArray());
|
||||
}
|
||||
|
||||
public function upload()
|
||||
{
|
||||
echo View::make('skinlib.upload')->with('user', $this->user)->with('with_out_filter', true)->render();
|
||||
}
|
||||
|
||||
public function handleUpload()
|
||||
{
|
||||
$this->checkUpload(isset($_POST['type']) ? $_POST['type'] : "");
|
||||
|
||||
$t = new Texture();
|
||||
$t->name = $_POST['name'];
|
||||
$t->type = $_POST['type'];
|
||||
$t->likes = 1;
|
||||
$t->hash = Utils::upload($_FILES['file']);
|
||||
$t->size = ceil($_FILES['file']['size'] / 1024);
|
||||
$t->public = ($_POST['public'] == 'true') ? "1" : "0";
|
||||
$t->uploader = $this->user->uid;
|
||||
$t->upload_at = Utils::getTimeFormatted();
|
||||
|
||||
$cost = $t->size * (($t->public == "1") ? Option::get('score_per_storage') : Option::get('private_score_per_storage'));
|
||||
|
||||
if ($this->user->getScore() < $cost)
|
||||
View::json('积分不够啦', 7);
|
||||
|
||||
$results = Texture::where('hash', $t->hash)->get();
|
||||
|
||||
if (!$results->isEmpty()) {
|
||||
foreach ($results as $result) {
|
||||
if ($result->type == $t->type) {
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '已经有人上传过这个材质了,直接添加到衣柜使用吧~',
|
||||
'tid' => $result->tid
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$t->save();
|
||||
|
||||
$this->user->setScore($cost, 'minus');
|
||||
|
||||
if ($this->user->closet->add($t->tid, $t->name)) {
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '材质 '.$_POST['name'].' 上传成功',
|
||||
'tid' => $t->tid
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
Validate::checkPost(['tid']);
|
||||
|
||||
$result = Texture::find($_POST['tid']);
|
||||
|
||||
if (!$result)
|
||||
View::json('Unexistent texture.', 1);
|
||||
|
||||
if ($result->uploader != $this->user->uid && !$this->user->is_admin)
|
||||
View::json('你不是这个材质的上传者哦', 1);
|
||||
|
||||
// check if file occupied
|
||||
if (Texture::where('hash', $result['hash'])->count() == 1)
|
||||
\Storage::remove("./textures/".$result['hash']);
|
||||
|
||||
$this->user->setScore($result->size * Option::get('score_per_storage'), 'plus');
|
||||
|
||||
if ($result->delete())
|
||||
View::json('材质已被成功删除', 0);
|
||||
}
|
||||
|
||||
public function privacy($tid)
|
||||
{
|
||||
$t = Texture::find($tid);
|
||||
|
||||
if (!$t) View::json('Unexistent texture.', 1);
|
||||
|
||||
if ($t->uploader != $this->user->uid && !$this->user->is_admin)
|
||||
View::json('你不是这个材质的上传者哦', 1);
|
||||
|
||||
if ($t->setPrivacy(!$t->public)) {
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '材质已被设为'.($t->public == "0" ? "隐私" : "公开"),
|
||||
'public' => $t->public
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function rename() {
|
||||
Validate::checkPost(['tid', 'new_name']);
|
||||
Validate::textureName($_POST['new_name']);
|
||||
|
||||
$t = Texture::find($_POST['tid']);
|
||||
|
||||
if (!$t) View::json('材质不存在', 1);
|
||||
|
||||
if ($t->uploader != $this->user->uid && !$this->user->is_admin)
|
||||
View::json('你不是这个材质的上传者哦', 1);
|
||||
|
||||
$t->name = $_POST['new_name'];
|
||||
|
||||
if ($t->save()) {
|
||||
View::json('材质名称已被成功设置为'.$_POST['new_name'], 0);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkUpload($type)
|
||||
{
|
||||
Validate::textureName(Utils::getValue('name', $_POST));
|
||||
|
||||
if (!Utils::getValue('file', $_FILES))
|
||||
View::json('你还没有选择任何文件哟', 1);
|
||||
|
||||
if (!isset($_POST['public']) || ($_POST['public'] != 0 && $_POST['public'] != 1))
|
||||
View::json('非法参数', 1);
|
||||
|
||||
if ($_FILES['file']['type'] == "image/png" || $_FILES['file']['type'] == "image/x-png")
|
||||
{
|
||||
// if error occured while uploading file
|
||||
if ($_FILES['file']["error"] > 0)
|
||||
View::json($_FILES['file']["error"], 1);
|
||||
|
||||
$size = getimagesize($_FILES['file']["tmp_name"]);
|
||||
$ratio = $size[0] / $size[1];
|
||||
|
||||
if ($type == "steve" || $type == "alex") {
|
||||
if ($ratio != 2 && $ratio != 1)
|
||||
View::json("不是有效的皮肤文件(宽 {$size[0]},高 {$size[1]})", 1);
|
||||
} elseif ($type == "cape") {
|
||||
if ($ratio != 2)
|
||||
View::json("不是有效的披风文件(宽 {$size[0]},高 {$size[1]})", 1);
|
||||
} else {
|
||||
View::json('非法参数', 1);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (Utils::getValue('file', $_FILES)) {
|
||||
View::json('文件格式不对哦', 1);
|
||||
} else {
|
||||
View::json('No file selected.', 1);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
155
app/Controllers/TextureController.php
Normal file
155
app/Controllers/TextureController.php
Normal file
File diff suppressed because one or more lines are too long
124
app/Controllers/UserController.php
Normal file
124
app/Controllers/UserController.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Texture;
|
||||
use App\Exceptions\E;
|
||||
use Utils;
|
||||
use View;
|
||||
|
||||
class UserController extends BaseController
|
||||
{
|
||||
private $action = "";
|
||||
private $user = null;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->action = isset($_GET['action']) ? $_GET['action'] : "";
|
||||
$this->user = new User($_SESSION['uid']);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
echo View::make('user.index')->with('user', $this->user)->render();
|
||||
}
|
||||
|
||||
public function sign()
|
||||
{
|
||||
if ($aquired_score = $this->user->sign()) {
|
||||
View::json([
|
||||
'errno' => 0,
|
||||
'msg' => '签到成功,获得了 '.$aquired_score.' 积分~',
|
||||
'score' => $this->user->getScore(),
|
||||
'remaining_time' => $this->user->canSign(true)
|
||||
]);
|
||||
} else {
|
||||
View::json($this->user->canSign(true).' 小时后才能再次签到哦~', 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
echo View::make('user.profile')->with('user', $this->user);
|
||||
}
|
||||
|
||||
public function handleProfile()
|
||||
{
|
||||
// handle changing nickname
|
||||
if ($this->action == "nickname") {
|
||||
if (!isset($_POST['new_nickname'])) throw new E('非法参数');
|
||||
|
||||
if (Utils::convertString($_POST['new_nickname']) != $_POST['new_nickname'])
|
||||
View::json('无效的昵称。昵称中包含了奇怪的字符。', 1);
|
||||
|
||||
if ($this->user->setNickName($_POST['new_nickname']))
|
||||
View::json('昵称已成功设置为 '.$_POST['new_nickname'], 0);
|
||||
// handle changing password
|
||||
} elseif ($this->action == "password") {
|
||||
if (!(isset($_POST['current_password']) && isset($_POST['new_password'])))
|
||||
throw new E('非法参数');
|
||||
|
||||
if (!$this->user->checkPasswd($_POST['current_password']))
|
||||
View::json('原密码错误', 1);
|
||||
|
||||
if (\Validate::password($_POST['new_password'])) {
|
||||
if ($this->user->changePasswd($_POST['new_password']))
|
||||
View::json('密码修改成功,请重新登录', 0);
|
||||
}
|
||||
// handle changing email
|
||||
} elseif ($this->action == "email") {
|
||||
if (!(isset($_POST['new_email']) && isset($_POST['password'])))
|
||||
throw new E('非法参数');
|
||||
|
||||
if (!filter_var($_POST['new_email'], FILTER_VALIDATE_EMAIL)) {
|
||||
View::json('邮箱格式错误', 3);
|
||||
}
|
||||
|
||||
if (!$this->user->checkPasswd($_POST['password']))
|
||||
View::json('密码错误', 1);
|
||||
|
||||
if ($this->user->setEmail($_POST['new_email']))
|
||||
View::json('邮箱修改成功,请重新登录', 0);
|
||||
|
||||
// handle deleting account
|
||||
} elseif ($this->action == "delete") {
|
||||
if (!isset($_POST['password']))
|
||||
throw new E('非法参数');
|
||||
|
||||
if (!$this->user->checkPasswd($_POST['password']))
|
||||
View::json('密码错误', 1);
|
||||
|
||||
if ($this->user->delete()) {
|
||||
setcookie('uid', '', time() - 3600, '/');
|
||||
setcookie('token', '', time() - 3600, '/');
|
||||
session_destroy();
|
||||
|
||||
View::json('账号已被成功删除', 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function config()
|
||||
{
|
||||
echo View::make('user.config')->with('user', $this->user);
|
||||
}
|
||||
|
||||
public function setAvatar()
|
||||
{
|
||||
if (!isset($_POST['tid'])) throw new E('Empty tid.');
|
||||
|
||||
$result = Texture::find($_POST['tid']);
|
||||
if ($result) {
|
||||
if ($result->type == "cape") throw new E('披风可不能设置为头像哦~', 1);
|
||||
|
||||
if ((new User($_SESSION['uid']))->setAvatar($_POST['tid'])) {
|
||||
View::json('设置成功!', 0);
|
||||
}
|
||||
} else {
|
||||
throw new E('材质不存在。', 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
78
app/Core/Config.php
Normal file
78
app/Core/Config.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing;
|
||||
|
||||
use \Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use \Blessing\Database\Schema;
|
||||
use \App\Exceptions\E;
|
||||
|
||||
class Config
|
||||
{
|
||||
public static function getDbConfig()
|
||||
{
|
||||
return require BASE_DIR.'/config/database.php';
|
||||
}
|
||||
|
||||
public static function getViewConfig()
|
||||
{
|
||||
return require BASE_DIR."/config/view.php";
|
||||
}
|
||||
|
||||
public static function checkPHPVersion()
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '5.5.9', '<'))
|
||||
throw new E('Blessing Skin Server v3 要求 PHP 版本不低于 5.5.9,当前版本为 '.phpversion(), -1, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check database config
|
||||
*
|
||||
* @param array $config
|
||||
* @return \MySQLi
|
||||
*/
|
||||
public static function checkDbConfig(Array $config)
|
||||
{
|
||||
// use error control to hide shitty connect warnings
|
||||
@$conn = new \mysqli($config['host'], $config['username'], $config['password'], $config['database'], $config['port']);
|
||||
|
||||
if ($conn->connect_error)
|
||||
throw new E("无法连接至 MySQL 服务器,请检查你的配置:".$conn->connect_error, $conn->connect_errno, true);
|
||||
|
||||
$conn->query("SET names 'utf8'");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function checkTableExist()
|
||||
{
|
||||
$tables = ['users', 'closets', 'players', 'textures', 'options'];
|
||||
|
||||
foreach ($tables as $table_name) {
|
||||
// prefix will be added automatically
|
||||
if (!Schema::hasTable($table_name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function checkCache()
|
||||
{
|
||||
$view_config = self::getViewConfig();
|
||||
|
||||
if (!is_dir($view_config['cache_path'])) {
|
||||
if (!mkdir($view_config['cache_path']))
|
||||
throw new E('缓存文件夹创建失败,请确认目录权限是否正确', -1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function checkDotEnvExist()
|
||||
{
|
||||
if (!file_exists(BASE_DIR."/.env"))
|
||||
exit('错误:.env 配置文件不存在');
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
200
app/Core/Database/Database.php
Normal file
200
app/Core/Database/Database.php
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing\Database;
|
||||
|
||||
use \App\Exceptions\E;
|
||||
use \Blessing\Config;
|
||||
|
||||
/**
|
||||
* Light-weight database helper
|
||||
*
|
||||
* @author <h@prinzeugen.net>
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
/**
|
||||
* Instance of MySQLi
|
||||
*
|
||||
* @var null
|
||||
*/
|
||||
private $connection = null;
|
||||
|
||||
/**
|
||||
* Connection config
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $config = null;
|
||||
|
||||
/**
|
||||
* Table name to do operations in
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $table_name = "";
|
||||
|
||||
/**
|
||||
* Construct with table name and another config optionally
|
||||
*
|
||||
* @param string $table_name
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct($config = null)
|
||||
{
|
||||
$this->config = is_null($config) ? Config::getDbConfig() : $config;
|
||||
|
||||
@$this->connection = new \mysqli(
|
||||
$this->config['host'],
|
||||
$this->config['username'],
|
||||
$this->config['password'],
|
||||
$this->config['database'],
|
||||
$this->config['port']
|
||||
);
|
||||
|
||||
if ($this->connection->connect_error)
|
||||
throw new E("Could not connect to MySQL database. Check your configuration:".
|
||||
$this->connection->connect_error, $this->connection->connect_errno, true);
|
||||
|
||||
$this->connection->query("SET names 'utf8'");
|
||||
}
|
||||
|
||||
public function table($table_name, $no_prefix = false)
|
||||
{
|
||||
if ($this->connection->real_escape_string($table_name) == $table_name) {
|
||||
$this->table_name = $no_prefix ? $table_name : $this->config['prefix'].$table_name;
|
||||
return $this;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Table name contains invalid characters', 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function query($sql)
|
||||
{
|
||||
// compile patterns
|
||||
$sql = str_replace('{table}', $this->table_name, $sql);
|
||||
|
||||
$result = $this->connection->query($sql);
|
||||
if ($this->connection->error)
|
||||
throw new E("Database query error: ".$this->connection->error.", Statement: ".$sql, -1);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function fetchArray($sql)
|
||||
{
|
||||
return $this->query($sql)->fetch_array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select records from table
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @param array $condition, see function `where`
|
||||
* @param string $table, which table to operate
|
||||
* @param boolean $dont_fetch_array, return resources if true
|
||||
* @return array|resources
|
||||
*/
|
||||
public function select($key, $value, $condition = null, $table = null, $dont_fetch_array = false)
|
||||
{
|
||||
$table = is_null($table) ? $this->table_name : $table;
|
||||
|
||||
if (isset($condition['where'])) {
|
||||
$sql = "SELECT * FROM $table".$this->where($condition);
|
||||
} else {
|
||||
$sql = "SELECT * FROM $table WHERE $key='$value'";
|
||||
}
|
||||
|
||||
if ($dont_fetch_array) {
|
||||
return $this->query($sql);
|
||||
} else {
|
||||
return $this->fetchArray($sql);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function insert($data, $table = null)
|
||||
{
|
||||
$keys = "";
|
||||
$values = "";
|
||||
$table = is_null($table) ? $this->table_name : $table;
|
||||
|
||||
foreach($data as $key => $value) {
|
||||
if ($value == end($data)) {
|
||||
$keys .= '`'.$key.'`';
|
||||
$values .= '"'.$value.'"';
|
||||
} else {
|
||||
$keys .= '`'.$key.'`,';
|
||||
$values .= '"'.$value.'", ';
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO $table ({$keys}) VALUES ($values)";
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function has($key, $value, $table = null)
|
||||
{
|
||||
return ($this->getNumRows($key, $value, $table) != 0) ? true : false;
|
||||
}
|
||||
|
||||
public function hasTable($table_name)
|
||||
{
|
||||
$sql = "SELECT table_name FROM `INFORMATION_SCHEMA`.`TABLES` WHERE (table_name = '$table_name') AND TABLE_SCHEMA='".Config::getDbConfig()['database']."'";
|
||||
return ($this->query($sql)->num_rows != 0) ? true : false;
|
||||
}
|
||||
|
||||
public function update($key, $value, $condition = null, $table = null)
|
||||
{
|
||||
$table = is_null($table) ? $this->table_name : $table;
|
||||
return $this->query("UPDATE $table SET `$key`='$value'".$this->where($condition));
|
||||
}
|
||||
|
||||
public function delete($condition = null, $table = null)
|
||||
{
|
||||
$table = is_null($table) ? $this->table_name : $table;
|
||||
return $this->query("DELETE FROM $table".$this->where($condition));
|
||||
}
|
||||
|
||||
public function getNumRows($key, $value, $table = null)
|
||||
{
|
||||
$table = is_null($table) ? $this->table_name : $table;
|
||||
$sql = "SELECT * FROM $table WHERE $key='$value'";
|
||||
return $this->query($sql)->num_rows;
|
||||
}
|
||||
|
||||
public function getRecordNum($table = null)
|
||||
{
|
||||
$table = is_null($table) ? $this->table_name : $table;
|
||||
$sql = "SELECT * FROM $table WHERE 1";
|
||||
return $this->query($sql)->num_rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate where statement
|
||||
*
|
||||
* @param array $condition, e.g. array('where'=>'username="shit"', 'limit'=>10, 'order'=>'uid')
|
||||
* @return string
|
||||
*/
|
||||
private function where($condition)
|
||||
{
|
||||
$statement = "";
|
||||
if (isset($condition['where']) && $condition['where'] != "") {
|
||||
$statement .= ' WHERE '.$condition['where'];
|
||||
}
|
||||
if (isset($condition['order'])) {
|
||||
$statement .= ' ORDER BY `'.$condition['order'].'`';
|
||||
}
|
||||
if (isset($condition['limit'])) {
|
||||
$statement .= ' LIMIT '.$condition['limit'];
|
||||
}
|
||||
return $statement;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (!is_null($this->connection))
|
||||
$this->connection->close();
|
||||
}
|
||||
|
||||
}
|
||||
30
app/Core/Database/Migration.php
Normal file
30
app/Core/Database/Migration.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing\Database;
|
||||
|
||||
use Blessing\Storage;
|
||||
|
||||
class Migration
|
||||
{
|
||||
/**
|
||||
* Create tables, prefix will be added automatically
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function creatTables()
|
||||
{
|
||||
require BASE_DIR."/setup/tables.php";
|
||||
}
|
||||
|
||||
public static function __callStatic($method, $args)
|
||||
{
|
||||
if (strpos($method, 'import') !== false) {
|
||||
$filename = BASE_DIR."/setup/migrations/".snake_case($method).".php";
|
||||
if (Storage::exists($filename)) {
|
||||
return require $filename;
|
||||
}
|
||||
}
|
||||
throw new \InvalidArgumentException('Non-existent migration');
|
||||
}
|
||||
|
||||
}
|
||||
24
app/Core/Database/Schema.php
Normal file
24
app/Core/Database/Schema.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing\Database;
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
class Schema
|
||||
{
|
||||
/**
|
||||
* Facade for Illuminate\Database\Schema
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public static function __callStatic($method, $args)
|
||||
{
|
||||
// the instance of capusle has been set as global
|
||||
$instance = Capsule::schema();
|
||||
|
||||
return call_user_func_array([$instance, $method], $args);
|
||||
}
|
||||
|
||||
}
|
||||
21
app/Core/Facades/App.php
Normal file
21
app/Core/Facades/App.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing\Facades;
|
||||
|
||||
use \Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @see \Blessing\Foundation\Application
|
||||
*/
|
||||
class App extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'app';
|
||||
}
|
||||
}
|
||||
18
app/Core/Facades/DB.php
Normal file
18
app/Core/Facades/DB.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing\Facades;
|
||||
|
||||
use \Illuminate\Support\Facades\Facade;
|
||||
|
||||
class DB extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'db';
|
||||
}
|
||||
}
|
||||
69
app/Core/Foundation/Application.php
Normal file
69
app/Core/Foundation/Application.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing\Foundation;
|
||||
|
||||
use \Illuminate\Container\Container;
|
||||
use \Blessing\Config;
|
||||
|
||||
class Application extends Container
|
||||
{
|
||||
private $version = null;
|
||||
|
||||
/**
|
||||
* Start Application
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$this->boot();
|
||||
|
||||
// Register Error Handler
|
||||
Boot::registerErrorHandler();
|
||||
|
||||
// Redirect if not installed
|
||||
Boot::checkInstallation();
|
||||
|
||||
// Start Route Dispatching
|
||||
Boot::bootRouter();
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
// Load Aliases
|
||||
Boot::loadServices();
|
||||
|
||||
// Check Runtime Environment
|
||||
Boot::checkRuntimeEnv();
|
||||
|
||||
// Register Facades
|
||||
Boot::registerFacades($this);
|
||||
|
||||
// Set Default Timezone to UTC+8
|
||||
Boot::setTimeZone();
|
||||
|
||||
// Load dotenv Configuration
|
||||
Boot::loadDotEnv(BASE_DIR);
|
||||
|
||||
// Boot Eloquent ORM
|
||||
Boot::bootEloquent(Config::getDbConfig());
|
||||
|
||||
// Start Session
|
||||
Boot::startSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version number of the application.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
if (is_null($this->version)) {
|
||||
$config = require BASE_DIR."/config/app.php";
|
||||
$this->version = $config['version'];
|
||||
}
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
}
|
||||
136
app/Core/Foundation/Boot.php
Normal file
136
app/Core/Foundation/Boot.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing\Foundation;
|
||||
|
||||
use \Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use \Illuminate\Support\Facades\Facade;
|
||||
use \Pecee\SimpleRouter\SimpleRouter as Router;
|
||||
use \App\Exceptions\ExceptionHandler;
|
||||
use \App\Exceptions\E;
|
||||
use \Blessing\Config;
|
||||
use \Blessing\Option;
|
||||
use \Blessing\Http;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static function loadDotEnv($dir)
|
||||
{
|
||||
if (Config::checkDotEnvExist()) {
|
||||
$dotenv = new \Dotenv\Dotenv($dir);
|
||||
$dotenv->load();
|
||||
}
|
||||
}
|
||||
|
||||
public static function registerFacades(Application $app)
|
||||
{
|
||||
Facade::setFacadeApplication($app);
|
||||
|
||||
$app->instance('app', $app);
|
||||
$app->bind('manager', \App\Services\PluginManager::class);
|
||||
$app->bind('db', \Blessing\Database\Database::class);
|
||||
}
|
||||
|
||||
public static function setTimeZone($timezone = 'Asia/Shanghai')
|
||||
{
|
||||
// set default time zone, UTC+8 for default
|
||||
date_default_timezone_set($timezone);
|
||||
}
|
||||
|
||||
public static function checkRuntimeEnv()
|
||||
{
|
||||
Config::checkPHPVersion();
|
||||
Config::checkCache();
|
||||
}
|
||||
|
||||
public static function checkInstallation($redirect_to = '../setup/index.php')
|
||||
{
|
||||
if (!Config::checkTableExist()) {
|
||||
Http::redirect($redirect_to);
|
||||
}
|
||||
|
||||
if (!is_dir(BASE_DIR.'/textures/')) {
|
||||
throw new E("检测到 `textures` 文件夹已被删除,请重新运行 <a href='./setup'>安装程序</a>,或者手动放置一个。", -1, true);
|
||||
}
|
||||
|
||||
if (\App::version() != Option::get('version', '')) {
|
||||
Http::redirect(Http::getBaseUrl().'/setup/update.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function loadServices()
|
||||
{
|
||||
// Set Aliases for App\Services
|
||||
$services = require BASE_DIR.'/config/services.php';
|
||||
|
||||
foreach ($services as $facade => $class) {
|
||||
class_alias($class, $facade);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register error handler
|
||||
*
|
||||
* @param object $handler Push specified whoops handler
|
||||
* @return void
|
||||
*/
|
||||
public static function registerErrorHandler($handler = null)
|
||||
{
|
||||
if (!is_null($handler) && $handler instanceof \Whoops\Handler\HandlerInterface) {
|
||||
$whoops = new \Whoops\Run;
|
||||
$whoops->pushHandler($handler);
|
||||
$whoops->register();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_ENV['APP_DEBUG'] !== "false") {
|
||||
// whoops: php errors for cool kids
|
||||
$whoops = new \Whoops\Run;
|
||||
$handler = ($_SERVER['REQUEST_METHOD'] == "GET") ?
|
||||
new \Whoops\Handler\PrettyPageHandler : new \Whoops\Handler\PlainTextHandler;
|
||||
$whoops->pushHandler($handler);
|
||||
$whoops->register();
|
||||
} else {
|
||||
// Register custom error handler
|
||||
ExceptionHandler::register();
|
||||
}
|
||||
}
|
||||
|
||||
public static function bootEloquent(Array $config)
|
||||
{
|
||||
if (Config::checkDbConfig($config)) {
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection($config);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
}
|
||||
}
|
||||
|
||||
public static function startSession()
|
||||
{
|
||||
session_start();
|
||||
}
|
||||
|
||||
public static function bootRouter()
|
||||
{
|
||||
/**
|
||||
* URL ends with slash will cause many reference problems
|
||||
*/
|
||||
if (Http::getUri() != "/" && substr(Http::getUri(), -1) == "/") {
|
||||
$url = substr(Http::getCurrentUrl(), 0, -1);
|
||||
Http::redirect($url);
|
||||
}
|
||||
|
||||
// Require Route Config
|
||||
Router::group([
|
||||
'exceptionHandler' => 'App\Exceptions\RouterExceptionHandler'
|
||||
], function() {
|
||||
require BASE_DIR.'/config/routes.php';
|
||||
});
|
||||
|
||||
// Start Route Dispatching
|
||||
Router::start('App\Controllers');
|
||||
}
|
||||
}
|
||||
102
app/Core/Http.php
Normal file
102
app/Core/Http.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing;
|
||||
|
||||
class Http
|
||||
{
|
||||
/**
|
||||
* HTTP redirect
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $msg Write message to session
|
||||
* @return void
|
||||
*/
|
||||
public static function redirect($url, $msg = "")
|
||||
{
|
||||
if ($msg !== "") $_SESSION['msg'] = $msg;
|
||||
|
||||
if (!headers_sent()) {
|
||||
header('Location: '.$url);
|
||||
} else {
|
||||
echo "<meta http-equiv='Refresh' content='0; URL=$url'>";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 301 Moved Permanently
|
||||
*
|
||||
* @param string $url
|
||||
* @return void
|
||||
*/
|
||||
public static function redirectPermanently($url)
|
||||
{
|
||||
http_response_code(301);
|
||||
header('Location: '.$url);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function getRealIP()
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
public static function setUri($uri)
|
||||
{
|
||||
$_SERVER["REQUEST_URI"] = $uri;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getUri()
|
||||
{
|
||||
return $_SERVER["REQUEST_URI"];
|
||||
}
|
||||
|
||||
public static function getBaseUrl()
|
||||
{
|
||||
$base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https://" : "http://";
|
||||
$base_url .= $_SERVER["SERVER_NAME"];
|
||||
$base_url .= ($_SERVER["SERVER_PORT"] == "80") ? "" : (":".$_SERVER["SERVER_PORT"]);
|
||||
|
||||
return $base_url;
|
||||
}
|
||||
|
||||
public static function getCurrentUrl()
|
||||
{
|
||||
return self::getBaseUrl().$_SERVER["REQUEST_URI"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate absolute url according to relative one
|
||||
*
|
||||
* @param string $relative
|
||||
* @return string
|
||||
*/
|
||||
public static function urlTo($relative)
|
||||
{
|
||||
return Option::get('site_url').$relative;
|
||||
}
|
||||
|
||||
public static function abort($code, $msg = "Something happened.", $is_json = false)
|
||||
{
|
||||
http_response_code((int)$code);
|
||||
if ($is_json) {
|
||||
View::json($msg, $code);
|
||||
} else {
|
||||
$config = require BASE_DIR."/config/view.php";
|
||||
if (file_exists($config['view_path']."/errors/".$code.".tpl")) {
|
||||
echo View::make('errors.'.$code)->with('code', $code)->with('message', $msg);
|
||||
} else {
|
||||
echo View::make('errors.e')->with('code', $code)->with('message', $msg);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
app/Core/Mail.php
Normal file
75
app/Core/Mail.php
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing;
|
||||
|
||||
use PHPMailer;
|
||||
|
||||
class Mail
|
||||
{
|
||||
/**
|
||||
* Instance of PHPMailer
|
||||
* @var object
|
||||
*/
|
||||
private $mail;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$mail = new PHPMailer();
|
||||
// $mail->SMTPDebug = 3; // Enable verbose debug output
|
||||
$mail->isSMTP(); // Set mailer to use SMTP
|
||||
$mail->Host = $_ENV['MAIL_HOST']; // Specify main and backup SMTP servers
|
||||
$mail->SMTPAuth = true; // Enable SMTP authentication
|
||||
$mail->Username = $_ENV['MAIL_USERNAME']; // SMTP username
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD']; // SMTP password
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION']; // Enable TLS encryption, `ssl` also accepted
|
||||
$mail->Port = $_ENV['MAIL_PORT']; // TCP port to connect to
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$this->mail = $mail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sender name
|
||||
*
|
||||
* @param string $name [description]
|
||||
*/
|
||||
public function from($name)
|
||||
{
|
||||
$this->mail->setFrom($_ENV['MAIL_USERNAME'], $name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function to($address)
|
||||
{
|
||||
$this->mail->addAddress($address);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function subject($subject)
|
||||
{
|
||||
$this->mail->Subject = $subject;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastError()
|
||||
{
|
||||
return $this->mailer->ErrorInfo;
|
||||
}
|
||||
|
||||
public function content($content)
|
||||
{
|
||||
$this->mail->isHTML(true);
|
||||
$this->mail->Body = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mail
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
return $this->mail->send();
|
||||
}
|
||||
|
||||
}
|
||||
76
app/Core/Option.php
Normal file
76
app/Core/Option.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing;
|
||||
|
||||
use \Illuminate\Database\Eloquent\Model;
|
||||
use \Exception;
|
||||
|
||||
class Option
|
||||
{
|
||||
public static function get($key, $default_value = null)
|
||||
{
|
||||
$option = OptionModel::where('option_name', $key)->first();
|
||||
|
||||
if (!$option) {
|
||||
if (!is_null($default_value)) {
|
||||
return $default_value;
|
||||
} else {
|
||||
$options = require BASE_DIR."/setup/options.php";
|
||||
|
||||
if (array_key_exists($key, $options)) {
|
||||
self::add($key, $options[$key]);
|
||||
return $options[$key];
|
||||
}
|
||||
throw new Exception('Unexistent option.', 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $option->option_value;
|
||||
}
|
||||
|
||||
public static function set($key, $value)
|
||||
{
|
||||
$option = OptionModel::where('option_name', $key)->first();
|
||||
|
||||
if (!$option)
|
||||
throw new Exception('Unexistent option.', 1);
|
||||
|
||||
$option->option_value = $value;
|
||||
return $option->save();
|
||||
}
|
||||
|
||||
public static function add($key, $value)
|
||||
{
|
||||
if (self::has($key))
|
||||
return true;
|
||||
|
||||
$option = new OptionModel;
|
||||
$option->option_name = $key;
|
||||
$option->option_value = $value;
|
||||
$option->save();
|
||||
}
|
||||
|
||||
public static function has($key)
|
||||
{
|
||||
try {
|
||||
OptionModel::where('option_name', $key)->firstOrFail();
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function delete($key)
|
||||
{
|
||||
OptionModel::where('option_name', $key)->delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class OptionModel extends Model
|
||||
{
|
||||
protected $table = 'options';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = ['option_value'];
|
||||
}
|
||||
165
app/Core/Storage.php
Normal file
165
app/Core/Storage.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing;
|
||||
|
||||
class Storage
|
||||
{
|
||||
/**
|
||||
* Read a file and return bin data
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string|bool
|
||||
*/
|
||||
public static function get($filename)
|
||||
{
|
||||
$result = file_get_contents($filename, 'r');
|
||||
if (false === $result) {
|
||||
throw new \Exception("Failed to read $filename.");
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function put($filename, $data)
|
||||
{
|
||||
return file_put_contents($filename, $data);
|
||||
}
|
||||
|
||||
public static function exists($filename)
|
||||
{
|
||||
return file_exists($filename);
|
||||
}
|
||||
|
||||
public static function hash($filename, $type = 'sha256')
|
||||
{
|
||||
return hash_file('sha256', $filename);
|
||||
}
|
||||
|
||||
public static function rename($fname, $new_fname)
|
||||
{
|
||||
if (false === rename($fname, $new_fname)) {
|
||||
throw new \Exception("Failed to rename $fname to $new_fname.");
|
||||
}
|
||||
return $new_fname;
|
||||
}
|
||||
|
||||
public static function size($filename)
|
||||
{
|
||||
if (self::exists($filename)) {
|
||||
return filesize($filename);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file
|
||||
*
|
||||
* @param $filename
|
||||
* @return $bool
|
||||
*/
|
||||
public static function remove($filename)
|
||||
{
|
||||
if (self::exists($filename)) {
|
||||
return unlink($filename);
|
||||
}
|
||||
}
|
||||
|
||||
public static function removeDir($dir)
|
||||
{
|
||||
$resource = opendir($dir);
|
||||
$size = 0;
|
||||
while($filename = @readdir($resource)) {
|
||||
if ($filename != "." && $filename != "..") {
|
||||
$path = $dir.$filename;
|
||||
if (is_dir($path)) {
|
||||
// recursion
|
||||
self::removeDir($path."/");
|
||||
} else {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($resource);
|
||||
|
||||
return rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively count the size of specified directory
|
||||
*
|
||||
* @param string $dir
|
||||
* @return int, total size in bytes
|
||||
*/
|
||||
public static function getDirSize($dir)
|
||||
{
|
||||
$resource = opendir($dir);
|
||||
$size = 0;
|
||||
while($filename = @readdir($resource)) {
|
||||
if ($filename != "." && $filename != "..") {
|
||||
$path = $dir.$filename;
|
||||
if (is_dir($path)) {
|
||||
// recursion
|
||||
$size += self::getDirSize($path);
|
||||
} else if (is_file($path)) {
|
||||
$size += filesize($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($resource);
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively count files of specified directory
|
||||
*
|
||||
* @param string $dir
|
||||
* @param $file_num
|
||||
* @return int, total size in bytes
|
||||
*/
|
||||
public static function getFileNum($dir, $file_num = 0)
|
||||
{
|
||||
$resource = opendir($dir);
|
||||
while($filename = readdir($resource)) {
|
||||
if ($filename != "." && $filename != "..") {
|
||||
$path = $dir.$filename;
|
||||
if (is_dir($path)) {
|
||||
// recursion
|
||||
$file_num = self::getFileNum($path, $file_num);
|
||||
} else {
|
||||
$file_num++;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($resource);
|
||||
return $file_num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory recursively
|
||||
*
|
||||
* @param string $source
|
||||
* @param string $dest
|
||||
* @return bool
|
||||
*/
|
||||
public static function copyDir($source, $dest)
|
||||
{
|
||||
if(!is_dir($source))
|
||||
return false;
|
||||
if(!is_dir($dest))
|
||||
mkdir($dest, 0777, true);
|
||||
|
||||
$handle = dir($source);
|
||||
|
||||
while($entry = $handle->read()) {
|
||||
if ($entry != "." && $entry != "..") {
|
||||
if (is_dir($source.'/'.$entry)) {
|
||||
// recursion
|
||||
self::copyDir($source.'/'.$entry, $dest.'/'.$entry);
|
||||
} else {
|
||||
@copy($source.'/'.$entry, $dest.'/'.$entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
60
app/Core/View.php
Normal file
60
app/Core/View.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace Blessing;
|
||||
|
||||
/**
|
||||
* Just a wrapper for Blade template engine
|
||||
*/
|
||||
class View
|
||||
{
|
||||
public static function show($view, $data = [], $mergeData = [])
|
||||
{
|
||||
echo self::make($view, $data, $mergeData)->render();
|
||||
}
|
||||
|
||||
public static function make($view, $data = [], $mergeData = [])
|
||||
{
|
||||
$config = require BASE_DIR."/config/view.php";
|
||||
$view_path = [$config['view_path']];
|
||||
$cache_path = $config['cache_path'];
|
||||
|
||||
$compiler = new \Xiaoler\Blade\Compilers\BladeCompiler($cache_path);
|
||||
|
||||
$engine = new \Xiaoler\Blade\Engines\CompilerEngine($compiler);
|
||||
$finder = new \Xiaoler\Blade\FileViewFinder($view_path);
|
||||
|
||||
$finder->addExtension('tpl');
|
||||
|
||||
$factory = new \Xiaoler\Blade\Factory($engine, $finder);
|
||||
|
||||
return $factory->make($view, $data, $mergeData);
|
||||
}
|
||||
|
||||
// function reload
|
||||
public static function json()
|
||||
{
|
||||
@header('Content-type: application/json; charset=utf-8');
|
||||
$args = func_get_args();
|
||||
if (count($args) == 1) {
|
||||
self::jsonCustom($args[0]);
|
||||
} elseif(count($args) == 2) {
|
||||
self::jsonException($args[0], $args[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private static function jsonCustom($array)
|
||||
{
|
||||
if (is_array($array))
|
||||
exit(json_encode($array));
|
||||
else
|
||||
throw new \Exception('The given arugument should be array.');
|
||||
}
|
||||
|
||||
private static function jsonException($msg, $errno)
|
||||
{
|
||||
exit(json_encode([
|
||||
'errno' => $errno,
|
||||
'msg' => $msg
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class ConfigureAdminMenu extends Event
|
||||
{
|
||||
public $menu;
|
||||
|
||||
public function __construct(array &$menu)
|
||||
{
|
||||
// Pass array by reference
|
||||
$this->menu = &$menu;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class ConfigureExploreMenu extends Event
|
||||
{
|
||||
public $menu;
|
||||
|
||||
public function __construct(array &$menu)
|
||||
{
|
||||
// Pass array by reference
|
||||
$this->menu = &$menu;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
|
||||
class ConfigureRoutes extends Event
|
||||
{
|
||||
public $router;
|
||||
|
||||
public function __construct(Router $router)
|
||||
{
|
||||
$this->router = $router;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class ConfigureUserMenu extends Event
|
||||
{
|
||||
public $menu;
|
||||
|
||||
public function __construct(array &$menu)
|
||||
{
|
||||
$this->menu = &$menu;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
abstract class Event
|
||||
{
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Player;
|
||||
|
||||
class PlayerProfileUpdated extends Event
|
||||
{
|
||||
public $player;
|
||||
|
||||
public function __construct(Player $player)
|
||||
{
|
||||
$this->player = $player;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Player;
|
||||
|
||||
class PlayerRetrieved extends Event
|
||||
{
|
||||
public $player;
|
||||
|
||||
public function __construct(Player $player)
|
||||
{
|
||||
$this->player = $player;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Player;
|
||||
|
||||
class PlayerWasAdded extends Event
|
||||
{
|
||||
public $player;
|
||||
|
||||
public function __construct(Player $player)
|
||||
{
|
||||
$this->player = $player;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class PlayerWasDeleted extends Event
|
||||
{
|
||||
public $playerName;
|
||||
|
||||
public function __construct($playerName)
|
||||
{
|
||||
$this->playerName = $playerName;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class PlayerWillBeAdded extends Event
|
||||
{
|
||||
public $playerName;
|
||||
|
||||
public function __construct($playerName)
|
||||
{
|
||||
$this->playerName = $playerName;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Player;
|
||||
|
||||
class PlayerWillBeDeleted extends Event
|
||||
{
|
||||
public $player;
|
||||
|
||||
public function __construct(Player $player)
|
||||
{
|
||||
$this->player = $player;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Services\Plugin;
|
||||
|
||||
class PluginBootFailed extends Event
|
||||
{
|
||||
public Plugin $plugin;
|
||||
|
||||
public function __construct(Plugin $plugin)
|
||||
{
|
||||
$this->plugin = $plugin;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Services\Plugin;
|
||||
|
||||
class PluginWasDeleted extends Event
|
||||
{
|
||||
public $plugin;
|
||||
|
||||
public function __construct(Plugin $plugin)
|
||||
{
|
||||
$this->plugin = $plugin;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Services\Plugin;
|
||||
|
||||
class PluginWasDisabled extends Event
|
||||
{
|
||||
public $plugin;
|
||||
|
||||
public function __construct(Plugin $plugin)
|
||||
{
|
||||
$this->plugin = $plugin;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Services\Plugin;
|
||||
|
||||
class PluginWasEnabled extends Event
|
||||
{
|
||||
public $plugin;
|
||||
|
||||
public function __construct(Plugin $plugin)
|
||||
{
|
||||
$this->plugin = $plugin;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class RenderingBadges extends Event
|
||||
{
|
||||
public $badges;
|
||||
|
||||
public function __construct(array &$badges)
|
||||
{
|
||||
$this->badges = &$badges;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class RenderingFooter extends Event
|
||||
{
|
||||
public $contents;
|
||||
|
||||
public function __construct(array &$contents)
|
||||
{
|
||||
$this->contents = &$contents;
|
||||
}
|
||||
|
||||
public function addContent(string $content)
|
||||
{
|
||||
$this->contents[] = $content;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class RenderingHeader extends Event
|
||||
{
|
||||
public $contents;
|
||||
|
||||
public function __construct(array &$contents)
|
||||
{
|
||||
$this->contents = &$contents;
|
||||
}
|
||||
|
||||
public function addContent(string $content)
|
||||
{
|
||||
$this->contents[] = $content;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class TextureDeleting extends Event
|
||||
{
|
||||
public $texture;
|
||||
|
||||
public function __construct(\App\Models\Texture $texture)
|
||||
{
|
||||
$this->texture = $texture;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserAuthenticated extends Event
|
||||
{
|
||||
public $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserLoggedIn extends Event
|
||||
{
|
||||
public $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserProfileUpdated extends Event
|
||||
{
|
||||
public $type;
|
||||
public $user;
|
||||
|
||||
public function __construct($type, User $user)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserRegistered extends Event
|
||||
{
|
||||
public $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class UserTryToLogin extends Event
|
||||
{
|
||||
public $identification;
|
||||
|
||||
public $authType;
|
||||
|
||||
public function __construct($identification, $authType)
|
||||
{
|
||||
$this->identification = $identification;
|
||||
$this->authType = $authType;
|
||||
}
|
||||
}
|
||||
39
app/Exceptions/E.php
Normal file
39
app/Exceptions/E.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class E extends \Exception
|
||||
{
|
||||
/**
|
||||
* Custom error handler
|
||||
*
|
||||
* @param string $message
|
||||
* @param integer $code
|
||||
* @param boolean $render, to show a error page
|
||||
*/
|
||||
function __construct($message = "Error occured.", $code = -1, $render = false)
|
||||
{
|
||||
parent::__construct($message, $code);
|
||||
if ($render) {
|
||||
$this->showErrorPage();
|
||||
} else {
|
||||
$this->showErrorJson();
|
||||
}
|
||||
}
|
||||
|
||||
private function showErrorJson()
|
||||
{
|
||||
$exception['errno'] = $this->code;
|
||||
$exception['msg'] = $this->message;
|
||||
@header('Content-type: application/json; charset=utf-8');
|
||||
exit(json_encode($exception));
|
||||
}
|
||||
|
||||
private function showErrorPage()
|
||||
{
|
||||
echo \View::make('errors.e')->with('code', $this->code)
|
||||
->with('message', $this->message)
|
||||
->render();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
70
app/Exceptions/ExceptionHandler.php
Normal file
70
app/Exceptions/ExceptionHandler.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class ExceptionHandler
|
||||
{
|
||||
public static function register()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] == "GET") {
|
||||
// use closure to pass parameters
|
||||
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
||||
self::handler(
|
||||
new \ErrorException($errstr, $errno, $errno, $errfile, $errline)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static function handler($e)
|
||||
{
|
||||
// do nothing if error reporting is turned off or suppressed with @
|
||||
if (error_reporting() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($e->getCode()) {
|
||||
case E_PARSE:
|
||||
case E_ERROR:
|
||||
case E_CORE_ERROR:
|
||||
case E_COMPILE_ERROR:
|
||||
case E_USER_ERROR:
|
||||
$level = 'Fatal Error';
|
||||
break;
|
||||
|
||||
case E_WARNING:
|
||||
case E_USER_WARNING:
|
||||
case E_COMPILE_WARNING:
|
||||
case E_RECOVERABLE_ERROR:
|
||||
$level = 'Warning';
|
||||
break;
|
||||
|
||||
case E_NOTICE:
|
||||
case E_USER_NOTICE:
|
||||
$level = 'Notice';
|
||||
break;
|
||||
|
||||
case E_STRICT:
|
||||
$level = 'Strict';
|
||||
break;
|
||||
|
||||
case E_DEPRECATED:
|
||||
case E_USER_DEPRECATED:
|
||||
$level = 'Deprecated';
|
||||
break;
|
||||
|
||||
default:
|
||||
$level = 'Type Unknown';
|
||||
break;
|
||||
}
|
||||
|
||||
echo \View::make('errors.exception')->with('level', $level)
|
||||
->with('message', $e->getMessage())
|
||||
->with('file', $e->getFile())
|
||||
->with('line', $e->getLine());
|
||||
|
||||
exit;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Passport\Exceptions\MissingScopeException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of the exception types that should not be reported.
|
||||
*/
|
||||
protected $dontReport = [
|
||||
\Illuminate\Auth\AuthenticationException::class,
|
||||
\Illuminate\Auth\Access\AuthorizationException::class,
|
||||
\Symfony\Component\HttpKernel\Exception\HttpException::class,
|
||||
\Illuminate\Validation\ValidationException::class,
|
||||
\Illuminate\Session\TokenMismatchException::class,
|
||||
ModelNotFoundException::class,
|
||||
PrettyPageException::class,
|
||||
];
|
||||
|
||||
public function render($request, Throwable $exception)
|
||||
{
|
||||
if ($exception instanceof ModelNotFoundException) {
|
||||
$model = $exception->getModel();
|
||||
if (Str::endsWith($model, 'Texture')) {
|
||||
$exception = new ModelNotFoundException(trans('skinlib.non-existent'));
|
||||
}
|
||||
} elseif ($exception instanceof MissingScopeException) {
|
||||
return json($exception->getMessage(), 403);
|
||||
}
|
||||
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
|
||||
protected function convertExceptionToArray(Throwable $e)
|
||||
{
|
||||
return [
|
||||
'message' => $e->getMessage(),
|
||||
'exception' => true,
|
||||
'trace' => collect($e->getTrace())
|
||||
->map(fn ($trace) => Arr::only($trace, ['file', 'line']))
|
||||
->filter(fn ($trace) => Arr::has($trace, 'file'))
|
||||
->map(function ($trace) {
|
||||
$trace['file'] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $trace['file']);
|
||||
|
||||
return $trace;
|
||||
})
|
||||
->filter(function ($trace) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$isFromPlugins = !app()->runningUnitTests()
|
||||
&& Str::contains($trace['file'], resolve('plugins')->getPluginsDirs()->all());
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
return Str::startsWith($trace['file'], 'app') || $isFromPlugins;
|
||||
})
|
||||
->values(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class PrettyPageException extends \Exception
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return response()->view('errors.pretty', ['code' => $this->code, 'message' => $this->message]);
|
||||
}
|
||||
}
|
||||
22
app/Exceptions/RouterExceptionHandler.php
Normal file
22
app/Exceptions/RouterExceptionHandler.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Pecee\Http\Request;
|
||||
use Pecee\SimpleRouter\RouterEntry;
|
||||
use Pecee\Handler\IExceptionHandler;
|
||||
|
||||
class RouterExceptionHandler implements IExceptionHandler
|
||||
{
|
||||
|
||||
public function handleError(Request $request, RouterEntry $router = null, \Exception $error)
|
||||
{
|
||||
if ($error->getCode() === 404) {
|
||||
\Http::abort(404, $error->getMessage(), ($_SERVER['REQUEST_METHOD'] == "POST"));
|
||||
} else {
|
||||
throw $error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use App\Services\PluginManager;
|
||||
use Blessing\Filter;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
public function index(Filter $filter)
|
||||
{
|
||||
$grid = [
|
||||
'layout' => [
|
||||
['md-6', 'md-6'],
|
||||
],
|
||||
'widgets' => [
|
||||
[
|
||||
[
|
||||
'admin.widgets.dashboard.usage',
|
||||
'admin.widgets.dashboard.notification',
|
||||
],
|
||||
['admin.widgets.dashboard.chart'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$grid = $filter->apply('grid:admin.index', $grid);
|
||||
|
||||
return view('admin.index', [
|
||||
'grid' => $grid,
|
||||
'sum' => [
|
||||
'users' => User::count(),
|
||||
'players' => Player::count(),
|
||||
'textures' => Texture::count(),
|
||||
'storage' => Texture::select('size')->sum('size'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function chartData()
|
||||
{
|
||||
$xAxis = Collection::times(31, fn ($i) => Carbon::today()->subDays(31 - $i)->isoFormat('l'));
|
||||
|
||||
$oneMonthAgo = Carbon::today()->subMonth();
|
||||
|
||||
$grouping = fn ($field) => fn ($item) => Carbon::parse($item->$field)->isoFormat('l');
|
||||
$mapping = fn ($item) => count($item);
|
||||
$aligning = fn ($data) => fn ($day) => $data->get($day) ?? 0;
|
||||
|
||||
/** @var Collection */
|
||||
$userRegistration = User::where('register_at', '>=', $oneMonthAgo)
|
||||
->select('register_at')
|
||||
->get()
|
||||
->groupBy($grouping('register_at'))
|
||||
->map($mapping);
|
||||
|
||||
/** @var Collection */
|
||||
$textureUploads = Texture::where('upload_at', '>=', $oneMonthAgo)
|
||||
->select('upload_at')
|
||||
->get()
|
||||
->groupBy($grouping('upload_at'))
|
||||
->map($mapping);
|
||||
|
||||
return [
|
||||
'labels' => [
|
||||
trans('admin.index.user-registration'),
|
||||
trans('admin.index.texture-uploads'),
|
||||
],
|
||||
'xAxis' => $xAxis,
|
||||
'data' => [
|
||||
$xAxis->map($aligning($userRegistration)),
|
||||
$xAxis->map($aligning($textureUploads)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function status(
|
||||
Request $request,
|
||||
PluginManager $plugins,
|
||||
Filesystem $filesystem,
|
||||
Filter $filter,
|
||||
) {
|
||||
$db = config('database.connections.'.config('database.default'));
|
||||
$dbType = Arr::get([
|
||||
'mysql' => 'MySQL/MariaDB',
|
||||
'sqlite' => 'SQLite',
|
||||
'pgsql' => 'PostgreSQL',
|
||||
], config('database.default'), '');
|
||||
|
||||
$enabledPlugins = $plugins->getEnabledPlugins()->map(fn ($plugin) => [
|
||||
'title' => trans($plugin->title), 'version' => $plugin->version,
|
||||
]);
|
||||
|
||||
if ($filesystem->exists(base_path('.git'))) {
|
||||
$process = new \Symfony\Component\Process\Process(
|
||||
['git', 'log', '--pretty=%H', '-1']
|
||||
);
|
||||
$process->run();
|
||||
$commit = $process->isSuccessful() ? trim($process->getOutput()) : '';
|
||||
}
|
||||
|
||||
$grid = [
|
||||
'layout' => [
|
||||
['md-6', 'md-6'],
|
||||
],
|
||||
'widgets' => [
|
||||
[
|
||||
['admin.widgets.status.info'],
|
||||
['admin.widgets.status.plugins'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$grid = $filter->apply('grid:admin.status', $grid);
|
||||
|
||||
return view('admin.status')
|
||||
->with('grid', $grid)
|
||||
->with('detail', [
|
||||
'bs' => [
|
||||
'version' => config('app.version'),
|
||||
'env' => config('app.env'),
|
||||
'debug' => config('app.debug') ? trans('general.yes') : trans('general.no'),
|
||||
'commit' => Str::limit($commit ?? '', 16, ''),
|
||||
'laravel' => app()->version(),
|
||||
],
|
||||
'server' => [
|
||||
'php' => PHP_VERSION,
|
||||
'web' => $request->server('SERVER_SOFTWARE', trans('general.unknown')),
|
||||
'os' => sprintf('%s %s %s', php_uname('s'), php_uname('r'), php_uname('m')),
|
||||
],
|
||||
'db' => [
|
||||
'type' => $dbType,
|
||||
'host' => Arr::get($db, 'host', ''),
|
||||
'port' => Arr::get($db, 'port', ''),
|
||||
'username' => Arr::get($db, 'username'),
|
||||
'database' => Arr::get($db, 'database'),
|
||||
'prefix' => Arr::get($db, 'prefix'),
|
||||
],
|
||||
])
|
||||
->with('plugins', $enabledPlugins);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Mail\ForgotPassword;
|
||||
use App\Models\Player;
|
||||
use App\Models\User;
|
||||
use App\Rules;
|
||||
use Blessing\Filter;
|
||||
use Blessing\Rejection;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Vectorface\Whip\Whip;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function login(Filter $filter)
|
||||
{
|
||||
$whip = new Whip();
|
||||
$ip = $whip->getValidIpAddress();
|
||||
$ip = $filter->apply('client_ip', $ip);
|
||||
|
||||
$rows = [
|
||||
'auth.rows.login.notice',
|
||||
'auth.rows.login.message',
|
||||
'auth.rows.login.form',
|
||||
'auth.rows.login.registration-link',
|
||||
];
|
||||
$rows = $filter->apply('auth_page_rows:login', $rows);
|
||||
|
||||
return view('auth.login', [
|
||||
'rows' => $rows,
|
||||
'extra' => [
|
||||
'tooManyFails' => cache(sha1('login_fails_'.$ip)) > 3,
|
||||
'recaptcha' => option('recaptcha_sitekey'),
|
||||
'invisible' => (bool) option('recaptcha_invisible'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function handleLogin(
|
||||
Request $request,
|
||||
Rules\Captcha $captcha,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
) {
|
||||
$data = $request->validate([
|
||||
'identification' => 'required',
|
||||
'password' => 'required|min:6|max:32',
|
||||
]);
|
||||
$identification = $data['identification'];
|
||||
$password = $data['password'];
|
||||
|
||||
$can = $filter->apply('can_login', null, [$identification, $password]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
// Guess type of identification
|
||||
$authType = filter_var($identification, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
|
||||
|
||||
$dispatcher->dispatch('auth.login.attempt', [$identification, $password, $authType]);
|
||||
event(new Events\UserTryToLogin($identification, $authType));
|
||||
|
||||
if ($authType == 'email') {
|
||||
$user = User::where('email', $identification)->first();
|
||||
} else {
|
||||
$player = Player::where('name', $identification)->first();
|
||||
$user = optional($player)->user;
|
||||
}
|
||||
|
||||
// Require CAPTCHA if user fails to login more than 3 times
|
||||
$whip = new Whip();
|
||||
$ip = $whip->getValidIpAddress();
|
||||
$ip = $filter->apply('client_ip', $ip);
|
||||
$loginFailsCacheKey = sha1('login_fails_'.$ip);
|
||||
$loginFails = (int) Cache::get($loginFailsCacheKey, 0);
|
||||
|
||||
if ($loginFails > 3) {
|
||||
$request->validate(['captcha' => ['required', $captcha]]);
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
return json(trans('auth.validation.user'), 2);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('auth.login.ready', [$user]);
|
||||
|
||||
if ($user->verifyPassword($request->input('password'))) {
|
||||
Session::forget('login_fails');
|
||||
Cache::forget($loginFailsCacheKey);
|
||||
|
||||
Auth::login($user, $request->input('keep'));
|
||||
|
||||
$dispatcher->dispatch('auth.login.succeeded', [$user]);
|
||||
event(new Events\UserLoggedIn($user));
|
||||
|
||||
return json(trans('auth.login.success'), 0, [
|
||||
'redirectTo' => $request->session()->pull('last_requested_path', url('/user')),
|
||||
]);
|
||||
} else {
|
||||
$loginFails++;
|
||||
Cache::put($loginFailsCacheKey, $loginFails, 3600);
|
||||
$dispatcher->dispatch('auth.login.failed', [$user, $loginFails]);
|
||||
|
||||
return json(trans('auth.validation.password'), 1, [
|
||||
'login_fails' => $loginFails,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function logout(Dispatcher $dispatcher)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$dispatcher->dispatch('auth.logout.before', [$user]);
|
||||
Auth::logout();
|
||||
$dispatcher->dispatch('auth.logout.after', [$user]);
|
||||
|
||||
return json(trans('auth.logout.success'), 0);
|
||||
}
|
||||
|
||||
public function register(Filter $filter)
|
||||
{
|
||||
$rows = [
|
||||
'auth.rows.register.notice',
|
||||
'auth.rows.register.form',
|
||||
];
|
||||
$rows = $filter->apply('auth_page_rows:register', $rows);
|
||||
|
||||
return view('auth.register', [
|
||||
'site_name' => option_localized('site_name'),
|
||||
'rows' => $rows,
|
||||
'extra' => [
|
||||
'player' => (bool) option('register_with_player_name'),
|
||||
'recaptcha' => option('recaptcha_sitekey'),
|
||||
'invisible' => (bool) option('recaptcha_invisible'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function handleRegister(
|
||||
Request $request,
|
||||
Rules\Captcha $captcha,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
) {
|
||||
$can = $filter->apply('can_register', null);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
$rule = option('register_with_player_name') ?
|
||||
['player_name' => [
|
||||
'required',
|
||||
new Rules\PlayerName(),
|
||||
'min:'.option('player_name_length_min'),
|
||||
'max:'.option('player_name_length_max'),
|
||||
]] :
|
||||
['nickname' => 'required|max:255'];
|
||||
$data = $request->validate(array_merge([
|
||||
'email' => 'required|email|unique:users',
|
||||
'password' => 'required|min:8|max:32',
|
||||
'captcha' => ['required', $captcha],
|
||||
], $rule));
|
||||
$playerName = $request->input('player_name');
|
||||
|
||||
$dispatcher->dispatch('auth.registration.attempt', [$data]);
|
||||
|
||||
if (
|
||||
option('register_with_player_name')
|
||||
&& Player::where('name', $playerName)->count() > 0
|
||||
) {
|
||||
return json(trans('user.player.add.repeated'), 1);
|
||||
}
|
||||
|
||||
// If amount of registered accounts of IP is more than allowed amount,
|
||||
// reject this registration.
|
||||
$whip = new Whip();
|
||||
$ip = $whip->getValidIpAddress();
|
||||
$ip = $filter->apply('client_ip', $ip);
|
||||
if (User::where('ip', $ip)->count() >= option('regs_per_ip')) {
|
||||
return json(trans('auth.register.max', ['regs' => option('regs_per_ip')]), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('auth.registration.ready', [$data]);
|
||||
|
||||
$user = new User();
|
||||
$user->email = $data['email'];
|
||||
$user->nickname = $data[option('register_with_player_name') ? 'player_name' : 'nickname'];
|
||||
$user->score = option('user_initial_score');
|
||||
$user->avatar = 0;
|
||||
$password = app('cipher')->hash($data['password'], config('secure.salt'));
|
||||
$password = $filter->apply('user_password', $password);
|
||||
$user->password = $password;
|
||||
$user->ip = $ip;
|
||||
$user->permission = User::NORMAL;
|
||||
$user->register_at = Carbon::now();
|
||||
$user->last_sign_at = Carbon::now()->subDay();
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('auth.registration.completed', [$user]);
|
||||
event(new Events\UserRegistered($user));
|
||||
|
||||
if (option('register_with_player_name')) {
|
||||
$dispatcher->dispatch('player.adding', [$playerName, $user]);
|
||||
|
||||
$player = new Player();
|
||||
$player->uid = $user->uid;
|
||||
$player->name = $playerName;
|
||||
$player->tid_skin = 0;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.added', [$player, $user]);
|
||||
event(new Events\PlayerWasAdded($player));
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('auth.login.ready', [$user]);
|
||||
Auth::login($user);
|
||||
$dispatcher->dispatch('auth.login.succeeded', [$user]);
|
||||
|
||||
return json(trans('auth.register.success'), 0);
|
||||
}
|
||||
|
||||
public function forgot()
|
||||
{
|
||||
if (config('mail.default') != '') {
|
||||
return view('auth.forgot', [
|
||||
'extra' => [
|
||||
'recaptcha' => option('recaptcha_sitekey'),
|
||||
'invisible' => (bool) option('recaptcha_invisible'),
|
||||
],
|
||||
]);
|
||||
} else {
|
||||
throw new PrettyPageException(trans('auth.forgot.disabled'), 8);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleForgot(
|
||||
Request $request,
|
||||
Rules\Captcha $captcha,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
) {
|
||||
$data = $request->validate([
|
||||
'email' => 'required|email',
|
||||
'captcha' => ['required', $captcha],
|
||||
]);
|
||||
|
||||
if (!config('mail.default')) {
|
||||
return json(trans('auth.forgot.disabled'), 1);
|
||||
}
|
||||
|
||||
$email = $data['email'];
|
||||
$dispatcher->dispatch('auth.forgot.attempt', [$email]);
|
||||
|
||||
$rateLimit = 180;
|
||||
$whip = new Whip();
|
||||
$ip = $whip->getValidIpAddress();
|
||||
$ip = $filter->apply('client_ip', $ip);
|
||||
$lastMailCacheKey = sha1('last_mail_'.$ip);
|
||||
$remain = $rateLimit + Cache::get($lastMailCacheKey, 0) - time();
|
||||
if ($remain > 0) {
|
||||
return json(trans('auth.forgot.frequent-mail'), 2);
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
if (!$user) {
|
||||
return json(trans('auth.forgot.unregistered'), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('auth.forgot.ready', [$user]);
|
||||
|
||||
$url = URL::temporarySignedRoute(
|
||||
'auth.reset',
|
||||
Carbon::now()->addHour(),
|
||||
['uid' => $user->uid],
|
||||
false
|
||||
);
|
||||
try {
|
||||
Mail::to($email)->send(new ForgotPassword(url($url)));
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
$dispatcher->dispatch('auth.forgot.failed', [$user, $url]);
|
||||
|
||||
return json(trans('auth.forgot.failed', ['msg' => $e->getMessage()]), 2);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('auth.forgot.sent', [$user, $url]);
|
||||
Cache::put($lastMailCacheKey, time(), 3600);
|
||||
|
||||
return json(trans('auth.forgot.success'), 0);
|
||||
}
|
||||
|
||||
public function reset(Request $request, $uid)
|
||||
{
|
||||
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
|
||||
|
||||
return view('auth.reset')->with('user', User::find($uid));
|
||||
}
|
||||
|
||||
public function handleReset(Dispatcher $dispatcher, Request $request, $uid)
|
||||
{
|
||||
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
|
||||
|
||||
['password' => $password] = $request->validate([
|
||||
'password' => 'required|min:8|max:32',
|
||||
]);
|
||||
$user = User::find($uid);
|
||||
|
||||
$dispatcher->dispatch('auth.reset.before', [$user, $password]);
|
||||
$user->changePassword($password);
|
||||
$dispatcher->dispatch('auth.reset.after', [$user, $password]);
|
||||
|
||||
return json(trans('auth.reset.success'), 0);
|
||||
}
|
||||
|
||||
public function captcha(\Gregwar\Captcha\CaptchaBuilder $builder)
|
||||
{
|
||||
$builder->build(100, 34);
|
||||
session(['captcha' => $builder->getPhrase()]);
|
||||
|
||||
return response($builder->output(), 200, [
|
||||
'Content-Type' => 'image/jpeg',
|
||||
'Cache-Control' => 'no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
public function fillEmail(Request $request)
|
||||
{
|
||||
$email = $request->validate(['email' => 'required|email|unique:users'])['email'];
|
||||
$user = $request->user();
|
||||
$user->email = $email;
|
||||
$user->save();
|
||||
|
||||
return redirect('/user');
|
||||
}
|
||||
|
||||
public function verify(Request $request)
|
||||
{
|
||||
if (!option('require_verification')) {
|
||||
throw new PrettyPageException(trans('user.verification.disabled'), 1);
|
||||
}
|
||||
|
||||
abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid'));
|
||||
|
||||
return view('auth.verify');
|
||||
}
|
||||
|
||||
public function handleVerify(Request $request, User $user)
|
||||
{
|
||||
abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid'));
|
||||
|
||||
['email' => $email] = $request->validate(['email' => 'required|email']);
|
||||
|
||||
if ($user->email !== $email) {
|
||||
return back()->with('errorMessage', trans('auth.verify.not-matched'));
|
||||
}
|
||||
|
||||
$user->verified = true;
|
||||
$user->save();
|
||||
|
||||
return redirect()->route('user.home');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use Blessing\Filter;
|
||||
use Blessing\Rejection;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ClosetController extends Controller
|
||||
{
|
||||
public function index(Filter $filter)
|
||||
{
|
||||
$grid = [
|
||||
'layout' => [
|
||||
['md-8', 'md-4'],
|
||||
],
|
||||
'widgets' => [
|
||||
[
|
||||
[
|
||||
'user.widgets.email-verification',
|
||||
'user.widgets.closet.list',
|
||||
],
|
||||
['shared.previewer'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$grid = $filter->apply('grid:user.closet', $grid);
|
||||
|
||||
return view('user.closet')
|
||||
->with('grid', $grid)
|
||||
->with('extra', [
|
||||
'unverified' => option('require_verification') && !auth()->user()->verified,
|
||||
'rule' => trans('user.player.player-name-rule.'.option('player_name_rule')),
|
||||
'length' => trans(
|
||||
'user.player.player-name-length',
|
||||
['min' => option('player_name_length_min'), 'max' => option('player_name_length_max')]
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getClosetData(Request $request)
|
||||
{
|
||||
$category = $request->input('category', 'skin');
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user
|
||||
->closet()
|
||||
->when(
|
||||
$category === 'cape',
|
||||
fn (Builder $query) => $query->where('type', 'cape'),
|
||||
fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
|
||||
)
|
||||
->when(
|
||||
$request->input('q'),
|
||||
fn (Builder $query, $search) => $query->like('item_name', $search)
|
||||
)
|
||||
->orderBy('texture_tid', 'DESC')
|
||||
->paginate((int) $request->input('perPage', 6));
|
||||
}
|
||||
|
||||
public function allIds()
|
||||
{
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user->closet()->pluck('texture_tid');
|
||||
}
|
||||
|
||||
public function add(
|
||||
Request $request,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
) {
|
||||
['tid' => $tid, 'name' => $name] = $request->validate([
|
||||
'tid' => 'required|integer',
|
||||
'name' => 'required',
|
||||
]);
|
||||
|
||||
/** @var User */
|
||||
$user = Auth::user();
|
||||
$name = $filter->apply('add_closet_item_name', $name, [$tid]);
|
||||
$dispatcher->dispatch('closet.adding', [$tid, $name, $user]);
|
||||
|
||||
$can = $filter->apply('can_add_closet_item', true, [$tid, $name]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
if ($user->score < option('score_per_closet_item')) {
|
||||
return json(trans('user.closet.add.lack-score'), 1);
|
||||
}
|
||||
|
||||
$tid = $request->tid;
|
||||
$texture = Texture::find($tid);
|
||||
if (!$texture) {
|
||||
return json(trans('user.closet.add.not-found'), 1);
|
||||
}
|
||||
|
||||
if (!$texture->public && ($texture->uploader != $user->uid && !$user->isAdmin())) {
|
||||
return json(trans('skinlib.show.private'), 1);
|
||||
}
|
||||
|
||||
if ($user->closet()->where('tid', $request->tid)->count() > 0) {
|
||||
return json(trans('user.closet.add.repeated'), 1);
|
||||
}
|
||||
|
||||
$user->closet()->attach($tid, ['item_name' => $request->name]);
|
||||
$user->score -= option('score_per_closet_item');
|
||||
$user->save();
|
||||
|
||||
$texture->likes++;
|
||||
$texture->save();
|
||||
|
||||
$dispatcher->dispatch('closet.added', [$texture, $name, $user]);
|
||||
|
||||
$uploader = User::find($texture->uploader);
|
||||
if ($uploader && $uploader->uid != $user->uid) {
|
||||
$uploader->score += option('score_award_per_like', 0);
|
||||
$uploader->save();
|
||||
}
|
||||
|
||||
return json(trans('user.closet.add.success', ['name' => $request->input('name')]), 0);
|
||||
}
|
||||
|
||||
public function rename(
|
||||
Request $request,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
$tid,
|
||||
) {
|
||||
['name' => $name] = $request->validate(['name' => 'required']);
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
|
||||
$name = $filter->apply('rename_closet_item_name', $name, [$tid]);
|
||||
$dispatcher->dispatch('closet.renaming', [$tid, $name, $user]);
|
||||
|
||||
$item = $user->closet()->find($tid);
|
||||
if (empty($item)) {
|
||||
return json(trans('user.closet.remove.non-existent'), 1);
|
||||
}
|
||||
$previousName = $item->pivot->item_name;
|
||||
|
||||
$can = $filter->apply('can_rename_closet_item', true, [$item, $name]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
$user->closet()->updateExistingPivot($tid, ['item_name' => $name]);
|
||||
|
||||
$dispatcher->dispatch('closet.renamed', [$item, $previousName, $user]);
|
||||
|
||||
return json(trans('user.closet.rename.success', ['name' => $name]), 0);
|
||||
}
|
||||
|
||||
public function remove(Dispatcher $dispatcher, Filter $filter, $tid)
|
||||
{
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
|
||||
$dispatcher->dispatch('closet.removing', [$tid, $user]);
|
||||
|
||||
$item = $user->closet()->find($tid);
|
||||
if (empty($item)) {
|
||||
return json(trans('user.closet.remove.non-existent'), 1);
|
||||
}
|
||||
|
||||
$can = $filter->apply('can_remove_closet_item', true, [$item]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
$user->closet()->detach($tid);
|
||||
|
||||
if (option('return_score')) {
|
||||
$user->score += option('score_per_closet_item');
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$texture = Texture::find($tid);
|
||||
$texture->likes--;
|
||||
$texture->save();
|
||||
|
||||
$dispatcher->dispatch('closet.removed', [$texture, $user]);
|
||||
|
||||
$uploader = User::find($texture->uploader);
|
||||
$uploader->score -= option('score_award_per_like', 0);
|
||||
$uploader->save();
|
||||
|
||||
return json(trans('user.closet.remove.success'), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ClosetManagementController extends Controller
|
||||
{
|
||||
public function list(User $user)
|
||||
{
|
||||
return $user->closet;
|
||||
}
|
||||
|
||||
public function add(Request $request, Dispatcher $dispatcher, User $user)
|
||||
{
|
||||
$tid = $request->input('tid');
|
||||
$texture = Texture::find($tid);
|
||||
if (!$texture) {
|
||||
return json(trans('user.closet.add.not-found'), 1);
|
||||
}
|
||||
|
||||
if ($user->closet()->where('tid', $request->tid)->count() > 0) {
|
||||
return json(trans('user.closet.add.repeated'), 1);
|
||||
}
|
||||
|
||||
$name = $texture->name;
|
||||
|
||||
$dispatcher->dispatch('closet.adding', [$tid, $name, $user]);
|
||||
|
||||
$user->closet()->attach($texture->tid, ['item_name' => $name]);
|
||||
|
||||
$dispatcher->dispatch('closet.added', [$texture, $name, $user]);
|
||||
|
||||
return json('', 0, compact('user', 'texture'));
|
||||
}
|
||||
|
||||
public function remove(Request $request, Dispatcher $dispatcher, User $user)
|
||||
{
|
||||
$tid = $request->input('tid');
|
||||
$dispatcher->dispatch('closet.removing', [$tid, $user]);
|
||||
|
||||
$item = $user->closet()->find($tid);
|
||||
if (empty($item)) {
|
||||
return json(trans('user.closet.remove.non-existent'), 1);
|
||||
}
|
||||
|
||||
$user->closet()->detach($tid);
|
||||
|
||||
$texture = Texture::find($tid);
|
||||
|
||||
$dispatcher->dispatch('closet.removed', [$texture, $user]);
|
||||
|
||||
return json('', 0, compact('user', 'texture'));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use DispatchesJobs;
|
||||
use ValidatesRequests;
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('home')
|
||||
->with('user', auth()->user())
|
||||
->with('site_description', option_localized('site_description'))
|
||||
->with('transparent_navbar', (bool) option('transparent_navbar', false))
|
||||
->with('fixed_bg', option('fixed_bg'))
|
||||
->with('hide_intro', option('hide_intro'))
|
||||
->with('home_pic_url', option('home_pic_url') ?: config('options.home_pic_url'));
|
||||
}
|
||||
|
||||
public function apiRoot()
|
||||
{
|
||||
$copyright = Arr::get(
|
||||
[
|
||||
'Powered with ❤ by Blessing Skin Server.',
|
||||
'Powered by Blessing Skin Server.',
|
||||
'Proudly powered by Blessing Skin Server.',
|
||||
'由 Blessing Skin Server 强力驱动。',
|
||||
'采用 Blessing Skin Server 搭建。',
|
||||
'使用 Blessing Skin Server 稳定运行。',
|
||||
'自豪地采用 Blessing Skin Server。',
|
||||
],
|
||||
option_localized('copyright_prefer', 0)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'blessing_skin' => config('app.version'),
|
||||
'spec' => 0,
|
||||
'copyright' => $copyright,
|
||||
'site_name' => option('site_name'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Plugin;
|
||||
use App\Services\PluginManager;
|
||||
use App\Services\Unzip;
|
||||
use Composer\CaBundle\CaBundle;
|
||||
use Composer\Semver\Comparator;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class MarketController extends Controller
|
||||
{
|
||||
public function marketData(PluginManager $manager)
|
||||
{
|
||||
$plugins = $this->fetch()->map(function ($item) use ($manager) {
|
||||
$plugin = $manager->get($item['name']);
|
||||
|
||||
if ($plugin) {
|
||||
$item['installed'] = $plugin->version;
|
||||
$item['can_update'] = Comparator::greaterThan($item['version'], $item['installed']);
|
||||
} else {
|
||||
$item['installed'] = false;
|
||||
}
|
||||
|
||||
$requirements = Arr::get($item, 'require', []);
|
||||
unset($item['require']);
|
||||
$item['dependencies'] = [
|
||||
'all' => $requirements,
|
||||
'unsatisfied' => $manager->getUnsatisfied(new Plugin('', $item)),
|
||||
];
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
public function download(Request $request, PluginManager $manager, Unzip $unzip)
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$plugins = $this->fetch();
|
||||
$metadata = $plugins->firstWhere('name', $name);
|
||||
|
||||
if (!$metadata) {
|
||||
return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
|
||||
}
|
||||
|
||||
$fakePlugin = new Plugin('', $metadata);
|
||||
$unsatisfied = $manager->getUnsatisfied($fakePlugin);
|
||||
$conflicts = $manager->getConflicts($fakePlugin);
|
||||
if ($unsatisfied->isNotEmpty() || $conflicts->isNotEmpty()) {
|
||||
$reason = $manager->formatUnresolved($unsatisfied, $conflicts);
|
||||
|
||||
return json(trans('admin.plugins.market.unresolved'), 1, compact('reason'));
|
||||
}
|
||||
|
||||
$path = tempnam(sys_get_temp_dir(), $name);
|
||||
$response = Http::withOptions([
|
||||
'sink' => $path,
|
||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||
])->get($metadata['dist']['url']);
|
||||
|
||||
if ($response->ok()) {
|
||||
$unzip->extract($path, $manager->getPluginsDirs()->first());
|
||||
|
||||
return json(trans('admin.plugins.market.install-success'), 0);
|
||||
} else {
|
||||
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected function fetch(): Collection
|
||||
{
|
||||
$lang = in_array(app()->getLocale(), config('plugins.locales'))
|
||||
? app()->getLocale()
|
||||
: config('app.fallback_locale');
|
||||
|
||||
$plugins = collect(explode(',', config('plugins.registry')))
|
||||
->map(function ($registry) use ($lang) {
|
||||
$registry = str_replace('{lang}', $lang, $registry);
|
||||
$response = Http::withOptions([
|
||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||
])->get(trim($registry));
|
||||
|
||||
if ($response->ok()) {
|
||||
return $response->json()['packages'];
|
||||
} else {
|
||||
throw new Exception(trans('admin.plugins.market.connection-error', ['error' => $response->status()]));
|
||||
}
|
||||
})
|
||||
->flatten(1);
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Notifications;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
|
||||
class NotificationsController extends Controller
|
||||
{
|
||||
public function send(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'receiver' => 'required|in:all,normal,uid,email',
|
||||
'uid' => 'required_if:receiver,uid|nullable|integer|exists:users',
|
||||
'email' => 'required_if:receiver,email|nullable|email|exists:users',
|
||||
'title' => 'required|max:20',
|
||||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$notification = new Notifications\SiteMessage($data['title'], $data['content']);
|
||||
|
||||
switch ($data['receiver']) {
|
||||
case 'all':
|
||||
$users = User::all();
|
||||
break;
|
||||
case 'normal':
|
||||
$users = User::where('permission', User::NORMAL)->get();
|
||||
break;
|
||||
case 'uid':
|
||||
$users = User::where('uid', $data['uid'])->get();
|
||||
break;
|
||||
case 'email':
|
||||
$users = User::where('email', $data['email'])->get();
|
||||
break;
|
||||
}
|
||||
Notification::send($users, $notification);
|
||||
|
||||
session(['sentResult' => trans('admin.notifications.send.success')]);
|
||||
|
||||
return redirect('/admin');
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
return auth()->user()
|
||||
->unreadNotifications
|
||||
->map(fn ($notification) => [
|
||||
'id' => $notification->id,
|
||||
'title' => $notification->data['title'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function read($id)
|
||||
{
|
||||
$notification = auth()
|
||||
->user()
|
||||
->unreadNotifications
|
||||
->first(fn ($notification) => $notification->id === $id);
|
||||
$notification->markAsRead();
|
||||
|
||||
$converter = new GithubFlavoredMarkdownConverter();
|
||||
|
||||
return [
|
||||
'title' => $notification->data['title'],
|
||||
'content' => $converter->convertToHtml($notification->data['content'] ?? '')->getContent(),
|
||||
'time' => $notification->created_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Facades\Option;
|
||||
use App\Services\OptionForm;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class OptionsController extends Controller
|
||||
{
|
||||
public function customize(Request $request)
|
||||
{
|
||||
$homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->text('home_pic_url')->hint();
|
||||
|
||||
$form->text('favicon_url')->hint()->description();
|
||||
|
||||
$form->checkbox('transparent_navbar')->label();
|
||||
|
||||
$form->checkbox('hide_intro')->label();
|
||||
|
||||
$form->checkbox('fixed_bg')->label();
|
||||
|
||||
$form->select('copyright_prefer')
|
||||
->option('0', 'Powered with ❤ by Blessing Skin Server.')
|
||||
->option('1', 'Powered by Blessing Skin Server.')
|
||||
->option('2', 'Proudly powered by Blessing Skin Server.')
|
||||
->option('3', '由 Blessing Skin Server 强力驱动。')
|
||||
->option('4', '采用 Blessing Skin Server 搭建。')
|
||||
->option('5', '使用 Blessing Skin Server 稳定运行。')
|
||||
->option('6', '自豪地采用 Blessing Skin Server。')
|
||||
->description();
|
||||
|
||||
$form->textarea('copyright_text')->rows(6)->description();
|
||||
})->handle(function () {
|
||||
Option::set('copyright_prefer_'.config('app.locale'), request('copyright_prefer'));
|
||||
Option::set('copyright_text_'.config('app.locale'), request('copyright_text'));
|
||||
});
|
||||
|
||||
$customJsCss = Option::form('customJsCss', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->textarea('custom_css', 'CSS')->rows(6);
|
||||
$form->textarea('custom_js', 'JavaScript')->rows(6);
|
||||
})->addMessage()->handle();
|
||||
|
||||
if ($request->isMethod('post') && $request->input('action') === 'color') {
|
||||
$navbar = $request->input('navbar');
|
||||
if ($navbar) {
|
||||
option(['navbar_color' => $navbar]);
|
||||
}
|
||||
|
||||
$sidebar = $request->input('sidebar');
|
||||
if ($sidebar) {
|
||||
option(['sidebar_color' => $sidebar]);
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin.customize', [
|
||||
'colors' => [
|
||||
'navbar' => [
|
||||
'primary', 'secondary', 'success', 'danger', 'indigo',
|
||||
'purple', 'pink', 'teal', 'cyan', 'dark', 'gray',
|
||||
'fuchsia', 'maroon', 'olive', 'navy',
|
||||
'lime', 'light', 'warning', 'white', 'orange',
|
||||
],
|
||||
'sidebar' => [
|
||||
'primary', 'warning', 'info', 'danger', 'success', 'indigo',
|
||||
'navy', 'purple', 'fuchsia', 'pink', 'maroon', 'orange',
|
||||
'lime', 'teal', 'olive',
|
||||
],
|
||||
],
|
||||
'forms' => [
|
||||
'homepage' => $homepage,
|
||||
'custom_js_css' => $customJsCss,
|
||||
],
|
||||
'extra' => [
|
||||
'navbar' => option('navbar_color'),
|
||||
'sidebar' => option('sidebar_color'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function score()
|
||||
{
|
||||
$rate = Option::form('rate', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->group('score_per_storage')->text('score_per_storage')->addon();
|
||||
|
||||
$form->group('private_score_per_storage')
|
||||
->text('private_score_per_storage')->addon()->hint();
|
||||
|
||||
$form->group('score_per_closet_item')
|
||||
->text('score_per_closet_item')->addon();
|
||||
|
||||
$form->checkbox('return_score')->label();
|
||||
|
||||
$form->group('score_per_player')->text('score_per_player')->addon();
|
||||
|
||||
$form->text('user_initial_score');
|
||||
})->handle();
|
||||
|
||||
$report = Option::form('report', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->text('reporter_score_modification')->description();
|
||||
|
||||
$form->text('reporter_reward_score');
|
||||
})->handle();
|
||||
|
||||
$sign = Option::form('sign', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->group('sign_score')
|
||||
->text('sign_score_from')->addon(trans('options.sign.sign_score.addon1'))
|
||||
->text('sign_score_to')->addon(trans('options.sign.sign_score.addon2'));
|
||||
|
||||
$form->group('sign_gap_time')->text('sign_gap_time')->addon();
|
||||
|
||||
$form->checkbox('sign_after_zero')->label()->hint();
|
||||
})->after(function () {
|
||||
$sign_score = request('sign_score_from').','.request('sign_score_to');
|
||||
Option::set('sign_score', $sign_score);
|
||||
})->with([
|
||||
'sign_score_from' => @explode(',', option('sign_score'))[0],
|
||||
'sign_score_to' => @explode(',', option('sign_score'))[1],
|
||||
])->handle();
|
||||
|
||||
$sharing = Option::form('sharing', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->group('score_award_per_texture')
|
||||
->text('score_award_per_texture')
|
||||
->addon(trans('general.user.score'));
|
||||
$form->checkbox('take_back_scores_after_deletion')->label();
|
||||
$form->group('score_award_per_like')
|
||||
->text('score_award_per_like')
|
||||
->addon(trans('general.user.score'));
|
||||
})->handle();
|
||||
|
||||
return view('admin.score', ['forms' => compact('rate', 'report', 'sign', 'sharing')]);
|
||||
}
|
||||
|
||||
public function options()
|
||||
{
|
||||
$general = Option::form('general', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->text('site_name');
|
||||
$form->text('site_description')->description();
|
||||
|
||||
$form->text('site_url')
|
||||
->hint()
|
||||
->format(function ($url) {
|
||||
if (Str::endsWith($url, '/')) {
|
||||
$url = substr($url, 0, -1);
|
||||
}
|
||||
|
||||
if (Str::endsWith($url, '/index.php')) {
|
||||
$url = substr($url, 0, -10);
|
||||
}
|
||||
|
||||
return $url;
|
||||
});
|
||||
|
||||
$form->checkbox('register_with_player_name')->label();
|
||||
$form->checkbox('require_verification')->label();
|
||||
|
||||
$form->text('regs_per_ip');
|
||||
|
||||
$form->group('max_upload_file_size')
|
||||
->text('max_upload_file_size')->addon('KB')
|
||||
->hint(trans('options.general.max_upload_file_size.hint', ['size' => ini_get('upload_max_filesize')]));
|
||||
|
||||
$form->group('max_texture_width')
|
||||
->text('max_texture_width')->addon('px')
|
||||
->hint(trans('options.general.max_texture_width.hint'));
|
||||
|
||||
$form->select('player_name_rule')
|
||||
->option('official', trans('options.general.player_name_rule.official'))
|
||||
->option('cjk', trans('options.general.player_name_rule.cjk'))
|
||||
->option('utf8', trans('options.general.player_name_rule.utf8'))
|
||||
->option('custom', trans('options.general.player_name_rule.custom'));
|
||||
|
||||
$form->text('custom_player_name_regexp')->hint()->placeholder();
|
||||
|
||||
$form->group('player_name_length')
|
||||
->text('player_name_length_min')
|
||||
->addon('~')
|
||||
->text('player_name_length_max')
|
||||
->addon(trans('options.general.player_name_length.suffix'));
|
||||
|
||||
$form->checkbox('auto_del_invalid_texture')->label()->hint();
|
||||
|
||||
$form->checkbox('allow_downloading_texture')->label();
|
||||
|
||||
$form->select('status_code_for_private')
|
||||
->option('403', '403 Forbidden')
|
||||
->option('404', '404 Not Found');
|
||||
|
||||
$form->text('texture_name_regexp')->hint()->placeholder();
|
||||
|
||||
$form->textarea('content_policy')->rows(3)->description();
|
||||
})->handle(function () {
|
||||
Option::set('site_name_'.config('app.locale'), request('site_name'));
|
||||
Option::set('site_description_'.config('app.locale'), request('site_description'));
|
||||
Option::set('content_policy_'.config('app.locale'), request('content_policy'));
|
||||
});
|
||||
|
||||
$announ = Option::form('announ', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->textarea('announcement')->rows(10)->description();
|
||||
})->renderWithoutTable()->handle(function () {
|
||||
Option::set('announcement_'.config('app.locale'), request('announcement'));
|
||||
});
|
||||
|
||||
$meta = Option::form('meta', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->text('meta_keywords')->hint();
|
||||
$form->text('meta_description')->hint();
|
||||
$form->textarea('meta_extras')->rows(6);
|
||||
})->handle();
|
||||
|
||||
$recaptcha = Option::form('recaptcha', 'reCAPTCHA', function ($form) {
|
||||
$form->text('recaptcha_sitekey', 'sitekey');
|
||||
$form->text('recaptcha_secretkey', 'secretkey');
|
||||
$form->checkbox('recaptcha_invisible')->label();
|
||||
})->handle();
|
||||
|
||||
return view('admin.options')
|
||||
->with('forms', compact('general', 'announ', 'meta', 'recaptcha'));
|
||||
}
|
||||
|
||||
public function resource(Request $request)
|
||||
{
|
||||
$resources = Option::form('resources', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->checkbox('force_ssl')->label()->hint();
|
||||
$form->checkbox('auto_detect_asset_url')->label()->description();
|
||||
|
||||
$form->text('cache_expire_time')->hint(OptionForm::AUTO_DETECT);
|
||||
$form->text('cdn_address')
|
||||
->hint(OptionForm::AUTO_DETECT)
|
||||
->description(OptionForm::AUTO_DETECT);
|
||||
})
|
||||
->type('primary')
|
||||
->hint(OptionForm::AUTO_DETECT)
|
||||
->after(function () {
|
||||
$cdnAddress = request('cdn_address');
|
||||
if ($cdnAddress == null) {
|
||||
$cdnAddress = '';
|
||||
}
|
||||
if (Str::endsWith($cdnAddress, '/')) {
|
||||
$cdnAddress = substr($cdnAddress, 0, -1);
|
||||
}
|
||||
Option::set('cdn_address', $cdnAddress);
|
||||
})
|
||||
->handle();
|
||||
|
||||
$cache = Option::form('cache', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->checkbox('enable_avatar_cache')->label();
|
||||
$form->checkbox('enable_preview_cache')->label();
|
||||
})
|
||||
->type('warning')
|
||||
->addButton([
|
||||
'text' => trans('options.cache.clear'),
|
||||
'type' => 'a',
|
||||
'class' => 'float-right',
|
||||
'style' => 'warning',
|
||||
'href' => '?clear-cache',
|
||||
])
|
||||
->addMessage(trans('options.cache.driver', ['driver' => config('cache.default')]), 'info');
|
||||
|
||||
if ($request->has('clear-cache')) {
|
||||
Cache::flush();
|
||||
$cache->addMessage(trans('options.cache.cleared'), 'success');
|
||||
}
|
||||
$cache->handle();
|
||||
|
||||
return view('admin.resource')->with('forms', compact('resources', 'cache'));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\PlayerWasAdded;
|
||||
use App\Events\PlayerWasDeleted;
|
||||
use App\Events\PlayerWillBeAdded;
|
||||
use App\Events\PlayerWillBeDeleted;
|
||||
use App\Models\Player;
|
||||
use App\Models\Texture;
|
||||
use App\Models\User;
|
||||
use App\Rules;
|
||||
use Blessing\Filter;
|
||||
use Blessing\Rejection;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PlayerController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(function (Request $request, $next) {
|
||||
/** @var Player */
|
||||
$player = $request->route('player');
|
||||
if ($player->user->isNot($request->user())) {
|
||||
return json(trans('admin.players.no-permission'), 1)
|
||||
->setStatusCode(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}, [
|
||||
'only' => ['delete', 'rename', 'setTexture', 'clearTexture'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Filter $filter)
|
||||
{
|
||||
$grid = [
|
||||
'layout' => [
|
||||
['md-6', 'md-6'],
|
||||
],
|
||||
'widgets' => [
|
||||
[
|
||||
[
|
||||
'user.widgets.players.list',
|
||||
'user.widgets.players.notice',
|
||||
],
|
||||
['shared.previewer'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$grid = $filter->apply('grid:user.player', $grid);
|
||||
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
|
||||
return view('user.player')
|
||||
->with('grid', $grid)
|
||||
->with('extra', [
|
||||
'count' => $user->players()->count(),
|
||||
'rule' => trans('user.player.player-name-rule.'.option('player_name_rule')),
|
||||
'length' => trans(
|
||||
'user.player.player-name-length',
|
||||
['min' => option('player_name_length_min'), 'max' => option('player_name_length_max')]
|
||||
),
|
||||
'score' => auth()->user()->score,
|
||||
'cost' => (int) option('score_per_player'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function list()
|
||||
{
|
||||
return Auth::user()->players;
|
||||
}
|
||||
|
||||
public function add(Request $request, Dispatcher $dispatcher, Filter $filter)
|
||||
{
|
||||
/** @var User */
|
||||
$user = Auth::user();
|
||||
|
||||
$name = $request->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
new Rules\PlayerName(),
|
||||
'min:'.option('player_name_length_min'),
|
||||
'max:'.option('player_name_length_max'),
|
||||
'unique:players',
|
||||
],
|
||||
])['name'];
|
||||
$name = $filter->apply('new_player_name', $name);
|
||||
|
||||
$dispatcher->dispatch('player.add.attempt', [$name, $user]);
|
||||
|
||||
$can = $filter->apply('can_add_player', true, [$name]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
if ($user->score < (int) option('score_per_player')) {
|
||||
return json(trans('user.player.add.lack-score'), 7);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('player.adding', [$name, $user]);
|
||||
event(new PlayerWillBeAdded($name));
|
||||
|
||||
$player = new Player();
|
||||
$player->uid = $user->uid;
|
||||
$player->name = $name;
|
||||
$player->tid_skin = 0;
|
||||
$player->tid_cape = 0;
|
||||
$player->save();
|
||||
|
||||
$user->score -= (int) option('score_per_player');
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('player.added', [$player, $user]);
|
||||
event(new PlayerWasAdded($player));
|
||||
|
||||
return json(trans('user.player.add.success', ['name' => $name]), 0, $player->toArray());
|
||||
}
|
||||
|
||||
public function delete(
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
Player $player,
|
||||
) {
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
$playerName = $player->name;
|
||||
|
||||
$dispatcher->dispatch('player.delete.attempt', [$player, $user]);
|
||||
|
||||
$can = $filter->apply('can_delete_player', true, [$player]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('player.deleting', [$player, $user]);
|
||||
event(new PlayerWillBeDeleted($player));
|
||||
|
||||
$player->delete();
|
||||
|
||||
if (option('return_score')) {
|
||||
$user->score += (int) option('score_per_player');
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('player.deleted', [$player, $user]);
|
||||
event(new PlayerWasDeleted($playerName));
|
||||
|
||||
return json(trans('user.player.delete.success', ['name' => $playerName]), 0);
|
||||
}
|
||||
|
||||
public function rename(
|
||||
Request $request,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
Player $player,
|
||||
) {
|
||||
$name = $request->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
new Rules\PlayerName(),
|
||||
'min:'.option('player_name_length_min'),
|
||||
'max:'.option('player_name_length_max'),
|
||||
Rule::unique('players')->ignoreModel($player),
|
||||
],
|
||||
])['name'];
|
||||
$name = $filter->apply('new_player_name', $name);
|
||||
|
||||
$dispatcher->dispatch('player.renaming', [$player, $name]);
|
||||
|
||||
$can = $filter->apply('can_rename_player', true, [$player, $name]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
$old = $player->replicate();
|
||||
$player->name = $name;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.renamed', [$player, $old]);
|
||||
|
||||
return json(
|
||||
trans('user.player.rename.success', ['old' => $old->name, 'new' => $name]),
|
||||
0,
|
||||
$player->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
public function setTexture(
|
||||
Request $request,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
Player $player,
|
||||
) {
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
|
||||
foreach (['skin', 'cape'] as $type) {
|
||||
$tid = $request->input($type);
|
||||
|
||||
$can = $filter->apply('can_set_texture', true, [$player, $type, $tid]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
if ($tid) {
|
||||
$texture = Texture::find($tid);
|
||||
if (empty($texture)) {
|
||||
return json(trans('skinlib.non-existent'), 1);
|
||||
}
|
||||
|
||||
if ($user->closet()->where('texture_tid', $tid)->doesntExist()) {
|
||||
return json(trans('user.closet.remove.non-existent'), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('player.texture.updating', [$player, $texture]);
|
||||
|
||||
$field = "tid_$type";
|
||||
$player->$field = $tid;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.texture.updated', [$player, $texture]);
|
||||
}
|
||||
}
|
||||
|
||||
return json(trans('user.player.set.success', ['name' => $player->name]), 0, $player->toArray());
|
||||
}
|
||||
|
||||
public function clearTexture(
|
||||
Request $request,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
Player $player,
|
||||
) {
|
||||
$types = $request->input('type', []);
|
||||
|
||||
foreach (['skin', 'cape'] as $type) {
|
||||
$can = $filter->apply('can_clear_texture', true, [$player, $type]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
|
||||
if ($request->has($type) || in_array($type, $types)) {
|
||||
$dispatcher->dispatch('player.texture.resetting', [$player, $type]);
|
||||
|
||||
$field = "tid_$type";
|
||||
$player->$field = 0;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.texture.reset', [$player, $type]);
|
||||
}
|
||||
}
|
||||
|
||||
return json(trans('user.player.clear.success', ['name' => $player->name]), 0, $player->toArray());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user