Compare commits
No commits in common. "dev" and "v2" have entirely different histories.
|
|
@ -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
|
|
||||||
45
.env.example
45
.env.example
|
|
@ -1,45 +0,0 @@
|
||||||
APP_DEBUG=false
|
|
||||||
APP_ENV=production
|
|
||||||
APP_FALLBACK_LOCALE=en
|
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
|
||||||
DB_HOST=localhost
|
|
||||||
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=
|
|
||||||
|
|
||||||
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
|
|
||||||
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 }}
|
|
||||||
32
.gitignore
vendored
32
.gitignore
vendored
|
|
@ -1,29 +1,3 @@
|
||||||
.env
|
textures/*
|
||||||
.sass-cache
|
config.php
|
||||||
coverage
|
setup/update_cache/*
|
||||||
.idea/
|
|
||||||
.cache/
|
|
||||||
.cache-loader/
|
|
||||||
vendor/*
|
|
||||||
storage/textures
|
|
||||||
storage/textures/*
|
|
||||||
storage/update_cache/*
|
|
||||||
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
|
|
||||||
|
|
|
||||||
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
|
|
||||||
10
.htaccess
Normal file
10
.htaccess
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
RewriteEngine on
|
||||||
|
RewriteBase /
|
||||||
|
|
||||||
|
RewriteRule ^([^/]*).json$ get.php?type=json&uname=$1 [L]
|
||||||
|
RewriteRule ^(skin|cape)/([^/-]*)(|-)(|alex|steve).png$ get.php?type=$1&model=$4&uname=$2 [L]
|
||||||
|
# 同时支持 UniSkinAPI 和 CustomSkinLoader API
|
||||||
|
RewriteRule ^(usm|csl)/([^/]*).json$ get.php?type=json&uname=$2&api=$1 [L]
|
||||||
|
RewriteRule ^(usm|csl)/textures/(.*)$ textures/$2 [L]
|
||||||
|
# 用于获取皮肤头像
|
||||||
|
RewriteRule ^avatar/(|[0-9]*/)([^/-]*).png$ get.php?type=avatar&uname=$2&size=$1 [L]
|
||||||
|
|
@ -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"]
|
|
||||||
12
LICENSE
12
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
The MIT License
|
||||||
|
|
||||||
Copyright (c) 2016-present The Blessing Skin Team
|
Copyright (c) 2016 Blessing Studio
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The above copyright notice and this permission notice shall be included in
|
||||||
copies or substantial portions of the Software.
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
SOFTWARE.
|
THE SOFTWARE.
|
||||||
|
|
|
||||||
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/),转载请注明。
|
|
||||||
283
README.md
283
README.md
|
|
@ -1,90 +1,247 @@
|
||||||
- [简体中文](./README-zh.md)
|
# Blessing Skin Server
|
||||||
- **English**
|
|
||||||
|
|
||||||
<p align="center"><img src="https://media.githubusercontent.com/media/bs-community/logo/main/logo.png"></p>
|
由于开发者学业原因,本项目已经暂停开发。开源协议已改为 MIT。
|
||||||
|
|
||||||
<p align="center">
|
优雅的开源 PHP Minecraft 皮肤站。[演示地址](https://skin.prinzeugen.net/)
|
||||||
<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!
|

|
||||||
|
|
||||||
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).
|
特性:
|
||||||
|
-----------
|
||||||
|
|
||||||
Blessing Skin is an open-source project written in PHP, which means you can deploy it freely on your own web server!
|
- 支持 [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 等程序进行数据对接
|
||||||
|
- 完善的用户管理后台以及配置页面
|
||||||
|
- 多种后台配色
|
||||||
|
- 可以获取由用户皮肤生成的头像(example.com/avatar/{{size}}/username.png)
|
||||||
|
|
||||||
## Features
|
环境要求:
|
||||||
|
-----------
|
||||||
|
|
||||||
- A fully functional skin hosting service
|
1. 一台支持 URL 重写的主机,Nginx、Apache 或 IIS
|
||||||
- Multiple player names can be owned by one user on the website
|
2. **PHP 版本 >= 5.4**
|
||||||
- Share your skins and capes online with skin library!
|
3. 目录的写权限~~(SAE 等不可写应用引擎的支持正在开发中)~~
|
||||||
- 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)
|
|
||||||
|
|
||||||
## Requirements
|
快速使用:
|
||||||
|
-----------
|
||||||
|
|
||||||
Blessing Skin has only a few system requirements. In most cases, these PHP extensions are already enabled.
|
1. 下载源码,重命名 `config.example.php` 为 `config.php` 并配置你的数据库连接信息
|
||||||
|
2. 运行 `./setup/install.php`
|
||||||
|
3. 如果你是用的是 Nginx,请配置你的 `nginx.conf` 并加入重写规则
|
||||||
|
4. 注册一个新账户或者使用 `安装时所配置的账户` (管理员账户)登录
|
||||||
|
5. 可以上传你的皮肤&披风啦
|
||||||
|
6. 在你所使用的皮肤 Mod 配置文件中加入你的地址
|
||||||
|
7. 完成啦~
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
## Quick Install
|
如果你使用 Apache 或者 IIS 作为 web 服务器(大部分的虚拟主机),那么恭喜你,我已经帮你把重写规则写好啦,开箱即用,无需任何配置~
|
||||||
|
|
||||||
Please read [Installation Guide](https://blessing.netlify.app/en/setup.html).
|
如果你使用 Nginx,请在你的 `nginx.conf` 中加入如下 rewrite 规则**(重要)**:
|
||||||
|
|
||||||
## Plugin System
|
```
|
||||||
|
rewrite ^/([^/]*).json$ /get.php?type=json&uname=$1 last;
|
||||||
|
rewrite ^/(skin|cape)/([^/-]*)(|-)(|alex|steve).png$ /get.php?type=$1&model=$4&uname=$2 last;
|
||||||
|
rewrite ^/(usm|csl)/([^/]*).json$ /get.php?type=json&uname=$2&api=$1 last;
|
||||||
|
rewrite ^/(usm|csl)/textures/(.*)$ /textures/$2 last;
|
||||||
|
# 用于获取皮肤头像
|
||||||
|
rewrite ^/avatar/(|[0-9]*/)([^/-]*).png$ /get.php?type=avatar&uname=$2&size=$1 last;
|
||||||
|
```
|
||||||
|
|
||||||
Blessing Skin provides an elegant and powerful plugin system, and you can attach plenty of functions and customization to your site via installing plugins.
|
如果你将皮肤站放在子目录中,你需要把重写规则改成类似于**这样**:
|
||||||
|
|
||||||
## Build From Source
|
```
|
||||||
|
rewrite ^/subdir/([^/]*).json$ /subdir/get.php?type=json&uname=$1 last;
|
||||||
|
```
|
||||||
|
|
||||||
Please refer to [Manual Build](https://blessing.netlify.app/build.html).
|
注意 `^/` 后和 `/get.php` 前都要加上你的子目录名。
|
||||||
|
|
||||||
## Internationalization
|
现在你可以访问 `http://example.com/username.json` 来得到你的首选 API 的 JSON 用户数据。另外一个 API 的 JSON 数据可以通过访问 `http://example.com/(usm|csl)/username.json` 得到。
|
||||||
|
|
||||||
Blessing Skin supports multiple languages, while currently supporting English, Simplified Chinese and Spanish.
|
上传完皮肤后,你就可以访问 `http://example.com/skin/username.png` 得到你的首选模型皮肤啦。 披风图片在这里:`http://example.com/cape/username.png` 。你还可以访问 `http://example.com/skin/username-(alex|steve).png` 来得到用户的 Alex/Steve 模型的皮肤文件(用户没上传则返回 404)。
|
||||||
|
|
||||||
If you are willing to contribute your translation, welcome to join [our Crowdin project](https://crowdin.com/project/blessing-skin).
|
数据对接:
|
||||||
|
------------
|
||||||
|
|
||||||
## Report Bugs
|
Blessing Skin Server 支持与 Authme、CrazyLogin、Discuz 等程序进行数据对接,只需在 `/admin/options.php` 中进行相应配置即可。
|
||||||
|
|
||||||
Read [FAQ](https://blessing.netlify.app/faq.html) and double check if your situation doesn't suit any case mentioned there before reporting.
|
如果在插件配置中修改过用户名或者密码的字段,请自行修改 `对接数据表用户名字段` 等信息。
|
||||||
|
|
||||||
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.
|
注意,`config.php` 中填写的数据库连接信息必须与被对接的程序的连接信息相同**(即同一个数据库)**。
|
||||||
|
|
||||||
## Related Links
|
如需适配其他程序,继承 `Database` 类并实现 `EncryptInterface` 与 `SyncInterface` 两个接口即可。
|
||||||
|
|
||||||
- [User Manual](https://blessing.netlify.app/en/)
|
客户端配置:
|
||||||
- [Plugins Development Documentation](https://bs-plugin.netlify.app/)
|
------------
|
||||||
|
|
||||||
## 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"` 字段按照你的 `config.php` 中配置的首选 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 程序是基于 The MIT License 开放源代码的自由软件,你可以遵照 MIT 协议来修改和重新发布这一程序。
|
||||||
|
|
||||||
|
程序原作者为 [@printempw](https://prinzeugen.net/),转载请注明。
|
||||||
|
|
|
||||||
114
admin/adduser.php
Normal file
114
admin/adduser.php
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-03-19 21:00:58
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-05-12 21:53:48
|
||||||
|
*/
|
||||||
|
require "../libraries/session.inc.php";
|
||||||
|
if (!$user->is_admin) Utils::redirect('../index.php', '看起来你并不是管理员');
|
||||||
|
View::show('admin/header', array('page_title' => "添加用户"));
|
||||||
|
$db = new Database\Database('users');
|
||||||
|
?>
|
||||||
|
<!-- Content Wrapper. Contains page content -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
批量添加用户
|
||||||
|
<small>Add Users</small>
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
a > i, button > i {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<?php
|
||||||
|
function showCallout($type, $id, $msg) {
|
||||||
|
echo "<div class='callout callout-".$type."'>$id".":".$_POST['username-'.$id]." $msg"."</div>";
|
||||||
|
}
|
||||||
|
if (isset($_POST['submit'])) {
|
||||||
|
for ($i = 1; $i <= (int)$_POST['submit']; $i++) {
|
||||||
|
if (User::checkValidUname($_POST['username-'.$i])) {
|
||||||
|
$password = ($_POST['password-'.$i] == "") ? '123456' : $_POST['password-'.$i];
|
||||||
|
if (strlen($password) < 16 && strlen($password) > 5) {
|
||||||
|
$user = new User($_POST['username-'.$i]);
|
||||||
|
if (!$user->is_registered) {
|
||||||
|
if ($user->register($password, 'added by admin')) {
|
||||||
|
if ($_FILES['skin-'.$i]['name'] != "") {
|
||||||
|
if ($user->setTexture('steve', $_FILES['skin-'.$i])) {
|
||||||
|
showCallout('success', $i, "皮肤上传成功!");
|
||||||
|
} else {
|
||||||
|
showCallout('danger', $i, "出现了奇怪的错误。。请联系作者 :(");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCallout('success', $i, "注册成功!密码 $password");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCallout('danger', $i, "注册失败.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCallout('danger', $i, "用户名已被注册。");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCallout('danger', $i, "无效的密码。密码长度应该大于 6 并小于 15。");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCallout('danger', $i, "无效的用户名。用户名只能包含数字,字母以及下划线。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?>
|
||||||
|
<form method="post" action="adduser.php" enctype="multipart/form-data">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-body table-responsive no-padding">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>用户名</th>
|
||||||
|
<th>密码(默认 123456)</th>
|
||||||
|
<th>上传皮肤</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody id="users">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="javascript:add();" style="float: right; margin-left: 10px;" class="btn btn-primary"><i class="fa fa-plus"></i>添加一个用户</a>
|
||||||
|
<button type="submit" name="submit" style="float: right;" class="btn btn-primary"><i class="fa fa-upload"></i>提交</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</section><!-- /.content -->
|
||||||
|
</div><!-- /.content-wrapper -->
|
||||||
|
<?php
|
||||||
|
$data['script'] = <<< 'EOT'
|
||||||
|
<script type="text/javascript" src="../assets/libs/bootstrap-fileinput/js/fileinput.min.js"></script>
|
||||||
|
<script type="text/javascript" src="../assets/libs/bootstrap-fileinput/js/fileinput_locale_zh.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var user_count = 1;
|
||||||
|
$(document).ready(function() {
|
||||||
|
add();
|
||||||
|
});
|
||||||
|
function add() {
|
||||||
|
var dom = '<tr id="user-'+user_count+'">'+
|
||||||
|
'<td>'+user_count+'</td>'+
|
||||||
|
'<td><input type="text" class="form-control" name="username-'+user_count+'"></td>'+
|
||||||
|
'<td><input type="password" class="form-control" name="password-'+user_count+'"></td>'+
|
||||||
|
'<td><input type="file" class="form-control" name="skin-'+user_count+'" data-show-preview="false" name="site_name" accept="image/png" ></td>'+
|
||||||
|
'</tr>';
|
||||||
|
$('#users').append($(dom));
|
||||||
|
$('input[type=file]').fileinput({showCaption: false, 'showUpload':false, 'language': 'zh'});
|
||||||
|
$('button[type=submit]').prop('value', user_count);
|
||||||
|
user_count++;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
EOT;
|
||||||
|
View::show('admin/footer', $data); ?>
|
||||||
81
admin/admin_ajax.php
Normal file
81
admin/admin_ajax.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-02-04 13:53:55
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-05-12 21:53:48
|
||||||
|
*/
|
||||||
|
require "../libraries/session.inc.php";
|
||||||
|
|
||||||
|
// Check token, won't allow non-admin user to access
|
||||||
|
if (!$user->is_admin) Utils::redirect('../index.php', '看起来你并不是管理员');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* No protection here,
|
||||||
|
* I don't think you wanna fuck yourself :(
|
||||||
|
*/
|
||||||
|
if (isset($_GET['action'])) {
|
||||||
|
$action = $_GET['action'];
|
||||||
|
$user = new User(isset($_GET['uname']) ? $_GET['uname'] : '');
|
||||||
|
|
||||||
|
if ($action == "upload") {
|
||||||
|
$type = isset($_GET['type']) ? $_GET['type'] : "skin";
|
||||||
|
$file = isset($_FILES['file']) ? $_FILES['file'] : null;
|
||||||
|
if (!is_null($file)) {
|
||||||
|
if ($user->setTexture($type, $file)) {
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "皮肤上传成功。";
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "出现了奇怪的错误。。请联系作者";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new E('你没有选择任何文件哦', 1);
|
||||||
|
}
|
||||||
|
} else if ($action == "change") {
|
||||||
|
if (User::checkValidPwd($_POST['passwd'])) {
|
||||||
|
$user->changePasswd($_POST['passwd']);
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "成功更改了 ".$_GET['uname']." 的密码。";
|
||||||
|
} // Will raise exception if password invalid
|
||||||
|
} else if ($action == "deleteAccount") {
|
||||||
|
$user->unRegister();
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "成功删除了该用户。";
|
||||||
|
} else if ($action == "deleteTexture") {
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
switch($i) {
|
||||||
|
case 1: $type = "steve"; break;
|
||||||
|
case 2: $type = "alex"; break;
|
||||||
|
case 3: $type = "cape"; break;
|
||||||
|
}
|
||||||
|
if ($_POST[$type] == "true" && $user->getTexture($type) != "") {
|
||||||
|
Utils::remove("./textures/".$user->getTexture($type));
|
||||||
|
$user->db->update('hash_'.$type, '', ['where' => "username='$user->uname'"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "成功地删除了该用户的所选材质。";
|
||||||
|
} else if ($action == "model") {
|
||||||
|
if (isset($_POST['model']) && $_POST['model'] == 'slim' || $_POST['model'] == 'default') {
|
||||||
|
$user->setPreference($_POST['model']);
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "成功地将用户 ".$_GET['uname']." 的优先皮肤模型更改为 ".$_POST['model']." 。";
|
||||||
|
} else {
|
||||||
|
throw new E('非法参数。', 1);
|
||||||
|
}
|
||||||
|
} else if ($action == "color") {
|
||||||
|
if (isset($_POST['color_scheme'])) {
|
||||||
|
$color_scheme = str_replace('_', '-', $_POST['color_scheme']);
|
||||||
|
Option::set('color_scheme', $color_scheme);
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "修改配色成功。";
|
||||||
|
} else {
|
||||||
|
throw new E('非法参数。', 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new E('非法参数。', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($json);
|
||||||
244
admin/customize.php
Normal file
244
admin/customize.php
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-03-19 14:34:21
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-05-12 21:53:48
|
||||||
|
*/
|
||||||
|
require "../libraries/session.inc.php";
|
||||||
|
if (!$user->is_admin) Utils::redirect('../index.php', '看起来你并不是管理员');
|
||||||
|
$data['style'] = <<< 'EOT'
|
||||||
|
<link rel="stylesheet" href="../assets/libs/AdminLTE/dist/css/skins/_all-skins.min.css">
|
||||||
|
<style>
|
||||||
|
.callout {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
EOT;
|
||||||
|
$data['page_title'] = "个性化";
|
||||||
|
View::show('admin/header', $data);
|
||||||
|
$db = new Database\Database('users');
|
||||||
|
?>
|
||||||
|
<!-- Content Wrapper. Contains page content -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
个性化
|
||||||
|
<small>Customize</small>
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="box box-primary">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">更改配色</h3>
|
||||||
|
</div><!-- /.box-header -->
|
||||||
|
<div class="box-body no-padding">
|
||||||
|
<table id="layout-skins-list" class="table table-striped bring-up nth-2-center">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>蓝色主题(默认)</td>
|
||||||
|
<td><a href="#" data-skin="skin-blue" class="btn btn-primary btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>蓝色主题-白色侧边栏</td>
|
||||||
|
<td><a href="#" data-skin="skin-blue-light" class="btn btn-primary btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>黄色主题</td>
|
||||||
|
<td><a href="#" data-skin="skin-yellow" class="btn btn-warning btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>黄色主题-白色侧边栏</td>
|
||||||
|
<td><a href="#" data-skin="skin-yellow-light" class="btn btn-warning btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>绿色主题</td>
|
||||||
|
<td><a href="#" data-skin="skin-green" class="btn btn-success btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>绿色主题-白色侧边栏</td>
|
||||||
|
<td><a href="#" data-skin="skin-green-light" class="btn btn-success btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>基佬紫</td>
|
||||||
|
<td><a href="#" data-skin="skin-purple" class="btn bg-purple btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>紫色主题-白色侧边栏</td>
|
||||||
|
<td><a href="#" data-skin="skin-purple-light" class="btn bg-purple btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>喜庆红(笑)</td>
|
||||||
|
<td><a href="#" data-skin="skin-red" class="btn btn-danger btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>红色主题-白色侧边栏</td>
|
||||||
|
<td><a href="#" data-skin="skin-red-light" class="btn btn-danger btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>高端黑</td>
|
||||||
|
<td><a href="#" data-skin="skin-black" class="btn bg-black btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>黑色主题-白色侧边栏</td>
|
||||||
|
<td><a href="#" data-skin="skin-black-light" class="btn bg-black btn-xs"><i class="fa fa-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div><!-- /.box-body -->
|
||||||
|
<div class="box-footer">
|
||||||
|
<button id="color-submit" class="btn btn-primary">提交</button>
|
||||||
|
<div id="msg" class="callout callout-info hide"></div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.box -->
|
||||||
|
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">Google Font CDN</h3>
|
||||||
|
</div><!-- /.box-header -->
|
||||||
|
<form method="post" action="customize.php">
|
||||||
|
<input type="hidden" name="option" value="adapter">
|
||||||
|
<div class="box-body">
|
||||||
|
<?php
|
||||||
|
if (isset($_POST['google_font_cdn'])) {
|
||||||
|
Option::set('google_font_cdn', $_POST['google_font_cdn']);
|
||||||
|
echo '<div class="callout callout-success">设置已保存。</div>';
|
||||||
|
} ?>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="value">
|
||||||
|
<select class="form-control" name="google_font_cdn">
|
||||||
|
<option <?php echo (Option::get('google_font_cdn') == 'google') ? 'selected="selected"' : ''; ?> value="google">Google Fonts(部分地区被墙)</option>
|
||||||
|
<option <?php echo (Option::get('google_font_cdn') == 'moefont') ? 'selected="selected"' : ''; ?> value="moefont">MoeFont CDN</option>
|
||||||
|
<option <?php echo (Option::get('google_font_cdn') == 'useso') ? 'selected="selected"' : ''; ?> value="useso">三六蛋 CDN(不支持 HTTPS)</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div><!-- /.box-body -->
|
||||||
|
<div class="box-footer">
|
||||||
|
<button type="submit" name="submit" class="btn btn-primary">提交</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">首页配置</h3>
|
||||||
|
</div><!-- /.box-header -->
|
||||||
|
<form method="post" action="customize.php">
|
||||||
|
<input type="hidden" name="option" value="adapter">
|
||||||
|
<div class="box-body">
|
||||||
|
<?php
|
||||||
|
if (isset($_POST['home_pic_url'])) {
|
||||||
|
Option::set('home_pic_url', $_POST['home_pic_url']);
|
||||||
|
echo '<div class="callout callout-success">设置已保存。</div>';
|
||||||
|
} ?>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="key">首页图片地址</td>
|
||||||
|
<td class="value">
|
||||||
|
<input type="text" data-toggle="tooltip" data-placement="bottom" title="相对与首页的路径或绝对路径。" class="form-control" name="home_pic_url" value="<?php echo Option::get('home_pic_url'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div><!-- /.box-body -->
|
||||||
|
<div class="box-footer">
|
||||||
|
<button type="submit" name="submit" class="btn btn-primary">提交</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-default">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">自定义 CSS/JavaScript
|
||||||
|
<i class="fa fa-question-circle" data-toggle="tooltip" data-placement="bottom" title="字符串将会被转义"></i>
|
||||||
|
</h3>
|
||||||
|
</div><!-- /.box-header -->
|
||||||
|
<form method="post" action="customize.php">
|
||||||
|
<input type="hidden" name="option" value="adapter">
|
||||||
|
<div class="box-body">
|
||||||
|
<?php
|
||||||
|
if (isset($_POST['custom_css']) && isset($_POST['custom_js'])) {
|
||||||
|
Option::set('custom_css', Utils::convertString($_POST['custom_css']));
|
||||||
|
Option::set('custom_js', Utils::convertString($_POST['custom_js']));
|
||||||
|
echo '<div class="callout callout-success">设置已保存。</div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="callout callout-info">内容将会被追加至每个页面的 <style> 和 <script> 标签中</div>';
|
||||||
|
} ?>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="key">CSS</td>
|
||||||
|
<td class="value">
|
||||||
|
<textarea name="custom_css" class="form-control" rows="3"><?php echo Option::get('custom_css'); ?></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">JavaScript</td>
|
||||||
|
<td class="value">
|
||||||
|
<textarea name="custom_js" class="form-control" rows="3"><?php echo Option::get('custom_js'); ?></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div><!-- /.box-body -->
|
||||||
|
<div class="box-footer">
|
||||||
|
<button type="submit" name="submit" class="btn btn-primary">提交</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section><!-- /.content -->
|
||||||
|
</div><!-- /.content-wrapper -->
|
||||||
|
<?php
|
||||||
|
$color_scheme = Option::get('color_scheme');
|
||||||
|
$data['script'] = <<< EOT
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Skin switcher
|
||||||
|
var current_skin = "$color_scheme";
|
||||||
|
$('#layout-skins-list [data-skin]').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var skin_name = $(this).data('skin');
|
||||||
|
$('body').removeClass(current_skin);
|
||||||
|
$('body').addClass(skin_name);
|
||||||
|
current_skin = skin_name;
|
||||||
|
});
|
||||||
|
$('#color-submit').click(function() {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: "admin_ajax.php?action=color",
|
||||||
|
dataType: "json",
|
||||||
|
data: { "color_scheme": current_skin },
|
||||||
|
beforeSend: function() {
|
||||||
|
showCallout('alert-info', '提交中。。');
|
||||||
|
},
|
||||||
|
success: function(json) {
|
||||||
|
console.log(json);
|
||||||
|
if (json.errno == 0) {
|
||||||
|
showCallout('alert-info', '设置已保存。');
|
||||||
|
} else {
|
||||||
|
showCallout('alert-warning', json.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
EOT;
|
||||||
|
View::show('admin/footer', $data); ?>
|
||||||
58
admin/index.php
Normal file
58
admin/index.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-02-03 14:39:50
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-05-12 21:53:49
|
||||||
|
*/
|
||||||
|
require "../libraries/session.inc.php";
|
||||||
|
if (!$user->is_admin) Utils::redirect('../index.php', '看起来你并不是管理员');
|
||||||
|
View::show('admin/header', array('page_title' => "仪表盘"));
|
||||||
|
$db = new Database\Database('users');
|
||||||
|
?>
|
||||||
|
<!-- Content Wrapper. Contains page content -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
仪表盘
|
||||||
|
<small>Dashboard</small>
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<a href="manage.php">
|
||||||
|
<span class="info-box-icon bg-aqua"><i class="fa fa-users"></i></span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">注册用户</span>
|
||||||
|
<span class="info-box-number"><?php echo $db->getRecordNum();?></span>
|
||||||
|
</div><!-- /.info-box-content -->
|
||||||
|
</a>
|
||||||
|
</div><!-- /.info-box -->
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-green"><i class="fa fa-files-o"></i></span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">上传材质总数</span>
|
||||||
|
<span class="info-box-number"><?php echo Utils::getFileNum(BASE_DIR."/textures/");?></span>
|
||||||
|
</div><!-- /.info-box-content -->
|
||||||
|
</div><!-- /.info-box -->
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-yellow"><i class="fa fa-hdd-o"></i></span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">占用空间大小</span>
|
||||||
|
<span class="info-box-number"><?php echo floor(Utils::getDirSize(BASE_DIR."/textures/")/1024)."KB";?></span>
|
||||||
|
</div><!-- /.info-box-content -->
|
||||||
|
</div><!-- /.info-box -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section><!-- /.content -->
|
||||||
|
</div><!-- /.content-wrapper -->
|
||||||
|
<?php
|
||||||
|
View::show('admin/footer'); ?>
|
||||||
29
admin/manage.php
Normal file
29
admin/manage.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-03-06 14:19:20
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-05-12 21:53:49
|
||||||
|
*/
|
||||||
|
require "../libraries/session.inc.php";
|
||||||
|
if (!$user->is_admin) Utils::redirect('../index.php', '看起来你并不是管理员');
|
||||||
|
View::show('admin/header', array('page_title' => "用户管理"));
|
||||||
|
$db = new Database\Database('users');
|
||||||
|
|
||||||
|
if (isset($_GET['show'])) {
|
||||||
|
View::show('admin/show', ['uid' => (int)$_GET['show']]);
|
||||||
|
} else {
|
||||||
|
View::show('admin/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['script'] = <<< 'EOT'
|
||||||
|
<script type="text/javascript" src="../assets/js/admin.utils.js"></script>
|
||||||
|
<script>
|
||||||
|
$('#page-select').on('change', function() {
|
||||||
|
window.location = "manage.php?page="+$(this).val();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
EOT;
|
||||||
|
if (isset($_GET['show']))
|
||||||
|
$data['script'] .= '<script type="text/javascript" src="../assets/js/preview.utils.js"></script>';
|
||||||
|
View::show('admin/footer', $data); ?>
|
||||||
232
admin/options.php
Normal file
232
admin/options.php
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-03-18 22:50:25
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-06-12 11:09:02
|
||||||
|
*/
|
||||||
|
require "../libraries/session.inc.php";
|
||||||
|
if (!$user->is_admin) Utils::redirect('../index.php', '看起来你并不是管理员');
|
||||||
|
View::show('admin/header', array('page_title' => "站点配置"));
|
||||||
|
$db = new Database\Database('users');
|
||||||
|
?>
|
||||||
|
<!-- Content Wrapper. Contains page content -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
站点配置
|
||||||
|
<small>Options</small>
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="box box-primary">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">常规选项</h3>
|
||||||
|
</div><!-- /.box-header -->
|
||||||
|
<form method="post" action="options.php">
|
||||||
|
<input type="hidden" name="option" value="general">
|
||||||
|
<div class="box-body">
|
||||||
|
<?php
|
||||||
|
if (isset($_POST['option']) && ($_POST['option'] == "general")) {
|
||||||
|
// pre-set user_can_register because it will not be posted if not checked
|
||||||
|
if (!isset($_POST['user_can_register'])) $_POST['user_can_register'] = '0';
|
||||||
|
foreach ($_POST as $key => $value) {
|
||||||
|
// remove slash if site_url is ended with slash
|
||||||
|
if ($key == "site_url" && preg_match("/.*(\/)$/", $value)) {
|
||||||
|
$value = substr($value, 0, -1);
|
||||||
|
}
|
||||||
|
if ($key != "option" && $key != "submit") {
|
||||||
|
Option::set($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo '<div class="callout callout-success">设置已保存。</div>';
|
||||||
|
} ?>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="key">站点标题</td>
|
||||||
|
<td class="value">
|
||||||
|
<input type="text" class="form-control" name="site_name" value="<?php echo Option::get('site_name'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">站点描述</td>
|
||||||
|
<td class="value">
|
||||||
|
<input type="text" class="form-control" name="site_description" value="<?php echo Option::get('site_description'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-toggle="tooltip" data-placement="bottom" title="以 http:// 开头,带上子目录(如果有)">
|
||||||
|
<td class="key">站点地址(URL)</td>
|
||||||
|
<td class="value">
|
||||||
|
<input type="text" class="form-control" name="site_url" value="<?php echo Option::get('site_url'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">开放注册</td>
|
||||||
|
<td class="value">
|
||||||
|
<label for="user_can_register">
|
||||||
|
<input <?php echo (Option::get('user_can_register') == '1') ? 'checked="true"' : ''; ?> type="checkbox" id="user_can_register" name="user_can_register" value="1">任何人都可以注册
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-toggle="tooltip" data-placement="bottom" title="默认皮肤文件请放置于 textures 目录下,并在此键入文件名。默认留空。">
|
||||||
|
<td class="key">新用户默认皮肤</td>
|
||||||
|
<td class="value">
|
||||||
|
<input type="text" class="form-control" name="user_default_skin" value="<?php echo Option::get('user_default_skin'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">每个 IP 限制注册数</td>
|
||||||
|
<td class="value">
|
||||||
|
<input type="text" class="form-control" name="regs_per_ip" value="<?php echo Option::get('regs_per_ip'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-toggle="tooltip" data-placement="bottom" title="PHP 限制:<?php echo ini_get('post_max_size'); ?>,定义在 php.ini 中。">
|
||||||
|
<td class="key">最大允许上传大小</td>
|
||||||
|
<td class="value">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" name="upload_max_size" value="<?php echo Option::get('upload_max_size'); ?>">
|
||||||
|
<span class="input-group-addon">KB</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">首选 JSON API</td>
|
||||||
|
<td class="value">
|
||||||
|
<select class="form-control" name="api_type">
|
||||||
|
<option <?php echo (Option::get('api_type') == '0') ? 'selected="selected"' : ''; ?> value="0">CustomSkinLoader API</option>
|
||||||
|
<option <?php echo (Option::get('api_type') == '1') ? 'selected="selected"' : ''; ?> value="1">UniversalSkinAPI</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">站点公告</td>
|
||||||
|
<td class="value">
|
||||||
|
<textarea name="announcement" class="form-control" rows="3"><?php echo Option::get('announcement'); ?></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div><!-- /.box-body -->
|
||||||
|
<div class="box-footer">
|
||||||
|
<button type="submit" name="submit" class="btn btn-primary">提交</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">数据对接配置</h3>
|
||||||
|
</div><!-- /.box-header -->
|
||||||
|
<form method="post" action="options.php">
|
||||||
|
<input type="hidden" name="option" value="adapter">
|
||||||
|
<div class="box-body">
|
||||||
|
<?php
|
||||||
|
if (isset($_POST['option']) && ($_POST['option'] == "adapter")) {
|
||||||
|
foreach ($_POST as $key => $value) {
|
||||||
|
if ($key != "option" && $key != "submit") {
|
||||||
|
Option::set($key, $value);
|
||||||
|
//echo $key."=".$value."<br />";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo '<div class="callout callout-success">设置已保存。</div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="callout callout-warning">如果你不知道下面这些是干什么的,请不要继续编辑。</div>';
|
||||||
|
} ?>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="key">数据对接适配器</td>
|
||||||
|
<td class="value">
|
||||||
|
<select class="form-control" name="data_adapter">
|
||||||
|
<option <?php echo (Option::get('data_adapter') == '') ? 'selected="selected"' : ''; ?> value="">不进行数据对接</option>
|
||||||
|
<option <?php echo (Option::get('data_adapter') == 'Authme') ? 'selected="selected"' : ''; ?> value="Authme">Authme</option>
|
||||||
|
<option <?php echo (Option::get('data_adapter') == 'Crazy') ? 'selected="selected"' : ''; ?> value="Crazy">CrazyLogin</option>
|
||||||
|
<option <?php echo (Option::get('data_adapter') == 'Discuz') ? 'selected="selected"' : ''; ?> value="Discuz">Discuz</option>
|
||||||
|
<option <?php echo (Option::get('data_adapter') == 'Phpwind') ? 'selected="selected"' : ''; ?> value="Phpwind">Phpwind</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">对接数据表名</td>
|
||||||
|
<td class="value">
|
||||||
|
<input type="text" class="form-control" name="data_table_name" value="<?php echo Option::get('data_table_name'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-toggle="tooltip" data-placement="bottom">
|
||||||
|
<td class="key">密码加密算法</td>
|
||||||
|
<td class="value">
|
||||||
|
<select class="form-control" name="encryption">
|
||||||
|
<option <?php echo (Option::get('encryption') == 'MD5') ? 'selected="selected"' : ''; ?> value="MD5">MD5</option>
|
||||||
|
<option <?php echo (Option::get('encryption') == 'SALTED2MD5') ? 'selected="selected"' : ''; ?> value="SALTED2MD5">SALTED2MD5</option>
|
||||||
|
<option <?php echo (Option::get('encryption') == 'SHA256') ? 'selected="selected"' : ''; ?> value="SHA256">SHA256</option>
|
||||||
|
<option <?php echo (Option::get('encryption') == 'CrazyCrypt1') ? 'selected="selected"' : ''; ?> value="CrazyCrypt1">CrazyCrypt1</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">对接数据表用户名字段</td>
|
||||||
|
<td class="value">
|
||||||
|
<input data-toggle="tooltip" data-placement="bottom" type="text" class="form-control" name="data_column_uname" value="<?php echo Option::get('data_column_uname'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">对接数据表密码字段</td>
|
||||||
|
<td class="value">
|
||||||
|
<input data-toggle="tooltip" data-placement="bottom" type="text" class="form-control" name="data_column_passwd" value="<?php echo Option::get('data_column_passwd'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="key">对接数据表 IP 字段</td>
|
||||||
|
<td class="value">
|
||||||
|
<input data-toggle="tooltip" data-placement="bottom" type="text" class="form-control" name="data_column_ip" value="<?php echo Option::get('data_column_ip'); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div><!-- /.box-body -->
|
||||||
|
<div class="box-footer">
|
||||||
|
<button type="submit" name="submit" class="btn btn-primary">提交</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-default collapsed-box">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">如何填写数据对接配置?</h3>
|
||||||
|
<div class="box-tools pull-right">
|
||||||
|
<button type="button" class="btn btn-box-tool" data-widget="collapse">
|
||||||
|
<i class="fa fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- /.box-tools -->
|
||||||
|
</div><!-- /.box-header -->
|
||||||
|
<div class="box-body">
|
||||||
|
<h4>对接数据表名</h4>
|
||||||
|
<p>所需要对接的程序的用户信息数据表,Authme 默认为 `authme`,CrazyLogin 默认为 `crazylogin_accounts`,Discuz 默认为 `pre_ucenter_members`,Phpwind 默认为 `windid_user`。请根据实际情况填写。</p>
|
||||||
|
<h4>密码加密算法</h4>
|
||||||
|
<p>皮肤站默认为 MD5。Authme 默认为 SHA256,CrazyLogin 为 CrazyCrypt1,Discuz 和 Phpwind 为 SALTED2MD5。没有需要的加密算法?请联系作者。</p>
|
||||||
|
<h4>对接数据表用户名字段</h4>
|
||||||
|
<p>如果你没有修改插件配置的话,请保持默认(`username`)。CrazyLogin 的话请将此字段改为 `name`。</p>
|
||||||
|
<h4>对接数据表密码字段</h4>
|
||||||
|
<p>同上,不要瞎球改。默认为 `password`</p>
|
||||||
|
<h4>对接数据表 IP 字段</h4>
|
||||||
|
<p>CrazyLogin 的话请将此字段改为 `ips`,Discuz 和 Phpwind 请改为 `regip`。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section><!-- /.content -->
|
||||||
|
</div><!-- /.content-wrapper -->
|
||||||
|
<?php
|
||||||
|
View::show('admin/footer'); ?>
|
||||||
29
admin/update.php
Normal file
29
admin/update.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-03-27 15:03:40
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-05-12 21:53:49
|
||||||
|
*/
|
||||||
|
require "../libraries/session.inc.php";
|
||||||
|
if (!$user->is_admin) Utils::redirect('../index.php', '看起来你并不是管理员');
|
||||||
|
$action = isset($_GET['action']) ? $_GET['action'] : "";
|
||||||
|
|
||||||
|
$updater = new Updater(Option::get('current_version'));
|
||||||
|
|
||||||
|
if ($action == "check" && $updater->newVersionAvailable()) {
|
||||||
|
exit(json_encode([
|
||||||
|
'new_version_available' => true,
|
||||||
|
'latest_version' => $updater->latest_version
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
View::show('admin/header', array('page_title' => "检查更新"));
|
||||||
|
|
||||||
|
if ($action == "download") {
|
||||||
|
View::show('admin/download');
|
||||||
|
} else {
|
||||||
|
View::show('admin/check');
|
||||||
|
}
|
||||||
|
|
||||||
|
View::show('admin/footer'); ?>
|
||||||
296
ajax.php
Normal file
296
ajax.php
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @Author: printempw
|
||||||
|
* @Date: 2016-01-16 23:01:33
|
||||||
|
* @Last Modified by: printempw
|
||||||
|
* @Last Modified time: 2016-04-03 10:16:00
|
||||||
|
*
|
||||||
|
* - login, register, logout
|
||||||
|
* - upload, change, delete
|
||||||
|
*
|
||||||
|
* All ajax requests will be handled here
|
||||||
|
*/
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Content-type: application/json');
|
||||||
|
|
||||||
|
$dir = dirname(__FILE__);
|
||||||
|
require "$dir/libraries/autoloader.php";
|
||||||
|
Database\Database::checkConfig();
|
||||||
|
|
||||||
|
if (isset($_POST['uname'])) {
|
||||||
|
$uname = $_POST['uname'];
|
||||||
|
if (User::checkValidUname($uname)) {
|
||||||
|
$user = new User($_POST['uname']);
|
||||||
|
} else {
|
||||||
|
throw new E('无效的用户名。用户名只能包含数字,字母以及下划线。', 3);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new E('空用户名。', 3);
|
||||||
|
}
|
||||||
|
$action = isset($_GET['action']) ? $_GET['action'] : null;
|
||||||
|
$json = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle requests from index.php
|
||||||
|
*/
|
||||||
|
if ($action == "login") {
|
||||||
|
if (checkPost()) {
|
||||||
|
if (!$user->is_registered) {
|
||||||
|
$json['errno'] = 2;
|
||||||
|
$json['msg'] = "用户不存在哦";
|
||||||
|
} else {
|
||||||
|
if ($user->checkPasswd($_POST['passwd'])) {
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = '登录成功,欢迎回来~';
|
||||||
|
$json['token'] = $user->getToken();
|
||||||
|
$_SESSION['token'] = $user->getToken();
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "用户名或密码不对哦";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($action == "register") {
|
||||||
|
if (checkPost('register')) {
|
||||||
|
if (!$user->is_registered) {
|
||||||
|
if (Option::get('user_can_register') == 1) {
|
||||||
|
if (User::checkValidPwd($_POST['passwd'])) {
|
||||||
|
// If amount of registered accounts of IP is more than allowed mounts,
|
||||||
|
// then reject the registration.
|
||||||
|
if ($user->db->getNumRows('ip', getRealIP()) < Option::get('regs_per_ip')) {
|
||||||
|
// use once md5 to encrypt password
|
||||||
|
if ($user->register($_POST['passwd'], getRealIP())) {
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "注册成功~";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 7;
|
||||||
|
$json['msg'] = "你最多只能注册 ".Option::get('regs_per_ip')." 个账户哦";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 7;
|
||||||
|
$json['msg'] = "残念。。本皮肤站已经关闭注册咯 QAQ";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 5;
|
||||||
|
$json['msg'] = "这个用户名已经被人注册辣,换一个吧";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPost() {
|
||||||
|
global $json;
|
||||||
|
if (!isset($_POST['passwd'])) {
|
||||||
|
$json['errno'] = 2;
|
||||||
|
$json['msg'] = "空密码。";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle request from user/index.php
|
||||||
|
*/
|
||||||
|
if ($action == "upload") {
|
||||||
|
if (Utils::getValue('token', $_SESSION) == $user->getToken()) {
|
||||||
|
if (checkFile()) {
|
||||||
|
if ($file = Utils::getValue('skin_file', $_FILES)) {
|
||||||
|
$model = (isset($_GET['model']) && $_GET['model'] == "steve") ? "steve" : "alex";
|
||||||
|
if ($user->setTexture($model, $file)) {
|
||||||
|
$json['skin']['errno'] = 0;
|
||||||
|
$json['skin']['msg'] = "皮肤上传成功!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($file = Utils::getValue('cape_file', $_FILES)) {
|
||||||
|
if ($user->setTexture('cape', $file)) {
|
||||||
|
$json['cape']['errno'] = 0;
|
||||||
|
$json['cape']['msg'] = "披风上传成功!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "无效的 token,请先登录。";
|
||||||
|
}
|
||||||
|
} else if ($action == "model") {
|
||||||
|
if (Utils::getValue('token', $_SESSION) == $user->getToken()) {
|
||||||
|
$new_model = ($user->getPreference() == "default") ? "slim" : "default";
|
||||||
|
$user->setPreference($new_model);
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "优先模型已经更改为 ".$user->getPreference()."。";
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "无效的 token,请先登录。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFile() {
|
||||||
|
global $json;
|
||||||
|
|
||||||
|
if (!(Utils::getValue('skin_file', $_FILES) || Utils::getValue('cape_file', $_FILES))) {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "什么文件都没有诶?";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for skin_file
|
||||||
|
*/
|
||||||
|
if (isset($_FILES['skin_file']) && ($_FILES['skin_file']['type'] == "image/png" ||
|
||||||
|
$_FILES['skin_file']['type'] == "image/x-png"))
|
||||||
|
{
|
||||||
|
// if error occured while uploading file
|
||||||
|
if ($_FILES['skin_file']["error"] > 0) {
|
||||||
|
$json['skin']['errno'] = 1;
|
||||||
|
$json['skin']['msg'] = $_FILES['skin_file']["error"];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($_FILES['skin_file']['size'] > (Option::get('upload_max_size')) * 1024) {
|
||||||
|
$json['skin']['errno'] = 1;
|
||||||
|
$json['skin']['msg'] = "本站最大只允许上传 ".Option::get('upload_max_size')." KB 的材质。";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$size = getimagesize($_FILES['skin_file']["tmp_name"]);
|
||||||
|
$ratio = $size[0] / $size[1];
|
||||||
|
if ($ratio != 2 && $ratio != 1) {
|
||||||
|
$json['skin']['errno'] = 1;
|
||||||
|
$json['skin']['msg'] = "不是有效的皮肤文件(宽 {$size[0]},高 {$size[1]})";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Utils::getValue('skin_file', $_FILES)) {
|
||||||
|
$json['skin']['errno'] = 1;
|
||||||
|
$json['skin']['msg'] = '错误的皮肤文件类型。';
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
$json['skin']['errno'] = 0;
|
||||||
|
$json['skin']['msg'] = '什么文件都没有诶?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for cape_file
|
||||||
|
*/
|
||||||
|
if (isset($_FILES['cape_file']) && ($_FILES['cape_file']['type'] == "image/png" ||
|
||||||
|
$_FILES['cape_file']['type'] == "image/x-png"))
|
||||||
|
{
|
||||||
|
// if error occured while uploading file
|
||||||
|
if ($_FILES['cape_file']["error"] > 0) {
|
||||||
|
$json['cape']['errno'] = 1;
|
||||||
|
$json['cape']['msg'] = $_FILES['cape_file']["error"];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($_FILES['cape_file']['size'] > (Option::get('upload_max_size')) * 1024) {
|
||||||
|
$json['cape']['errno'] = 1;
|
||||||
|
$json['cape']['msg'] = "本站最大只允许上传 ".(Option::get('upload_max_size') * 1024)." KB 的材质。";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$size = getimagesize($_FILES['cape_file']["tmp_name"]);
|
||||||
|
$ratio = $size[0] / $size[1];
|
||||||
|
if ($ratio != 2) {
|
||||||
|
$json['cape']['errno'] = 1;
|
||||||
|
$json['cape']['msg'] = "不是有效的披风文件(宽 {$size[0]},高 {$size[1]})";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Utils::getValue('cape_file', $_FILES)) {
|
||||||
|
$json['cape']['errno'] = 1;
|
||||||
|
$json['cape']['msg'] = '错误的披风文件类型。';
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
$json['cape']['errno'] = 0;
|
||||||
|
$json['cape']['msg'] = '什么文件都没有诶?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle requests from user/profile.php
|
||||||
|
*/
|
||||||
|
if ($action == "change") {
|
||||||
|
if (checkPost()) {
|
||||||
|
if (isset($_POST['new_passwd'])) {
|
||||||
|
if ($user->checkPasswd($_POST['passwd'])) {
|
||||||
|
$user->changePasswd($_POST['new_passwd']);
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "密码更改成功。请重新登录。";
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 2;
|
||||||
|
$json['msg'] = "原密码不对哦?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "新密码呢?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($action == "delete") {
|
||||||
|
if (isset($_SESSION['token']) && $_SESSION['token'] == $user->getToken()) {
|
||||||
|
if (checkPost()) {
|
||||||
|
if (!$user->is_admin) {
|
||||||
|
if ($user->checkPasswd($_POST['passwd'])) {
|
||||||
|
session_destroy();
|
||||||
|
$user->unRegister();
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "账号已经成功删除,再见~";
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 2;
|
||||||
|
$json['msg'] = "错误的密码。";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "管理员账号不能被删除哟~";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "无效的 token,请先登录。";
|
||||||
|
}
|
||||||
|
} else if ($action == "reset") {
|
||||||
|
if (isset($_SESSION['token']) && $_SESSION['token'] == $user->getToken()) {
|
||||||
|
if (checkPost()) {
|
||||||
|
if ($user->checkPasswd($_POST['passwd'])) {
|
||||||
|
$user->reset();
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = "重置成功。";
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 2;
|
||||||
|
$json['msg'] = "错误的密码。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = "无效的 token,请先登录。";
|
||||||
|
}
|
||||||
|
} else if ($action == "logout") {
|
||||||
|
if (Utils::getValue('token', $_SESSION)) {
|
||||||
|
session_destroy();
|
||||||
|
$json['errno'] = 0;
|
||||||
|
$json['msg'] = '成功登出 | ゚ ∀゚)ノ';
|
||||||
|
} else {
|
||||||
|
$json['errno'] = 1;
|
||||||
|
$json['msg'] = '并没有任何有效的 session。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$action) {
|
||||||
|
$json['errno'] = 6;
|
||||||
|
$json['msg'] = "无效的参数。不要乱 POST 玩哦。";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($json);
|
||||||
|
|
@ -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,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Player;
|
|
||||||
use App\Models\Texture;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Rules;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class PlayersManagementController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->middleware(function (Request $request, $next) {
|
|
||||||
/** @var Player */
|
|
||||||
$player = $request->route('player');
|
|
||||||
$owner = $player->user;
|
|
||||||
|
|
||||||
/** @var User */
|
|
||||||
$currentUser = $request->user();
|
|
||||||
|
|
||||||
if (
|
|
||||||
$owner->uid !== $currentUser->uid
|
|
||||||
&& $owner->permission >= $currentUser->permission
|
|
||||||
) {
|
|
||||||
return json(trans('admin.players.no-permission'), 1)
|
|
||||||
->setStatusCode(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
})->except(['list']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$query = $request->query('q');
|
|
||||||
|
|
||||||
return Player::usingSearchString($query)->paginate(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function name(
|
|
||||||
Player $player,
|
|
||||||
Request $request,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
) {
|
|
||||||
$name = $request->validate([
|
|
||||||
'player_name' => [
|
|
||||||
'required',
|
|
||||||
new Rules\PlayerName(),
|
|
||||||
'min:'.option('player_name_length_min'),
|
|
||||||
'max:'.option('player_name_length_max'),
|
|
||||||
'unique:players,name',
|
|
||||||
],
|
|
||||||
])['player_name'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('player.renaming', [$player, $name]);
|
|
||||||
|
|
||||||
$oldName = $player->name;
|
|
||||||
$player->name = $name;
|
|
||||||
$player->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('player.renamed', [$player, $oldName]);
|
|
||||||
|
|
||||||
return json(trans('admin.players.name.success', ['player' => $player->name]), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function owner(
|
|
||||||
Player $player,
|
|
||||||
Request $request,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
) {
|
|
||||||
$uid = $request->validate(['uid' => 'required|integer'])['uid'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('player.owner.updating', [$player, $uid]);
|
|
||||||
|
|
||||||
/** @var User */
|
|
||||||
$user = User::find($request->uid);
|
|
||||||
if (empty($user)) {
|
|
||||||
return json(trans('admin.users.operations.non-existent'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$player->uid = $uid;
|
|
||||||
$player->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('player.owner.updated', [$player, $user]);
|
|
||||||
|
|
||||||
return json(trans('admin.players.owner.success', [
|
|
||||||
'player' => $player->name,
|
|
||||||
'user' => $user->nickname,
|
|
||||||
]), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function texture(
|
|
||||||
Player $player,
|
|
||||||
Request $request,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
) {
|
|
||||||
$data = $request->validate([
|
|
||||||
'tid' => 'required|integer',
|
|
||||||
'type' => ['required', Rule::in(['skin', 'cape'])],
|
|
||||||
]);
|
|
||||||
$tid = (int) $data['tid'];
|
|
||||||
$type = $data['type'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('player.texture.updating', [$player, $type, $tid]);
|
|
||||||
|
|
||||||
if (Texture::where('tid', $tid)->doesntExist() && $tid !== 0) {
|
|
||||||
return json(trans('admin.players.textures.non-existent', ['tid' => $tid]), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$field = 'tid_'.$type;
|
|
||||||
$previousTid = $player->$field;
|
|
||||||
$player->$field = $tid;
|
|
||||||
$player->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('player.texture.updated', [$player, $type, $previousTid]);
|
|
||||||
|
|
||||||
return json(trans('admin.players.textures.success', ['player' => $player->name]), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(
|
|
||||||
Player $player,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
) {
|
|
||||||
$dispatcher->dispatch('player.deleting', [$player]);
|
|
||||||
|
|
||||||
$player->delete();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('player.deleted', [$player]);
|
|
||||||
|
|
||||||
return json(trans('admin.players.delete.success'), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Services\Plugin;
|
|
||||||
use App\Services\PluginManager;
|
|
||||||
use App\Services\Unzip;
|
|
||||||
use Composer\CaBundle\CaBundle;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
|
||||||
|
|
||||||
class PluginController extends Controller
|
|
||||||
{
|
|
||||||
public function config(PluginManager $plugins, $name)
|
|
||||||
{
|
|
||||||
$plugin = $plugins->get($name);
|
|
||||||
if ($plugin && $plugin->isEnabled()) {
|
|
||||||
if ($plugin->hasConfigClass()) {
|
|
||||||
return app()->call($plugin->getConfigClass().'@render');
|
|
||||||
} elseif ($plugin->hasConfigView()) {
|
|
||||||
return $plugin->getConfigView();
|
|
||||||
} else {
|
|
||||||
return abort(404, trans('admin.plugins.operations.no-config-notice'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return abort(404, trans('admin.plugins.operations.no-config-notice'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function readme(PluginManager $plugins, $name)
|
|
||||||
{
|
|
||||||
$plugin = $plugins->get($name);
|
|
||||||
if (empty($plugin)) {
|
|
||||||
return abort(404, trans('admin.plugins.operations.no-readme-notice'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$readmePath = $plugin->getReadme();
|
|
||||||
if (empty($readmePath)) {
|
|
||||||
return abort(404, trans('admin.plugins.operations.no-readme-notice'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$title = trans($plugin->title);
|
|
||||||
$path = $plugin->getPath().'/'.$readmePath;
|
|
||||||
$converter = new GithubFlavoredMarkdownConverter();
|
|
||||||
$content = $converter->convertToHtml(file_get_contents($path));
|
|
||||||
|
|
||||||
return view('admin.plugin.readme', compact('content', 'title'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function manage(Request $request, PluginManager $plugins)
|
|
||||||
{
|
|
||||||
$name = $request->input('name');
|
|
||||||
$plugin = $plugins->get($name);
|
|
||||||
|
|
||||||
if ($plugin) {
|
|
||||||
// Pass the plugin title through the translator.
|
|
||||||
$plugin->title = trans($plugin->title);
|
|
||||||
|
|
||||||
switch ($request->get('action')) {
|
|
||||||
case 'enable':
|
|
||||||
$result = $plugins->enable($name);
|
|
||||||
|
|
||||||
if ($result === true) {
|
|
||||||
return json(trans('admin.plugins.operations.enabled', ['plugin' => $plugin->title]), 0);
|
|
||||||
} else {
|
|
||||||
$reason = $plugins->formatUnresolved($result['unsatisfied'], $result['conflicts']);
|
|
||||||
|
|
||||||
return json(trans('admin.plugins.operations.unsatisfied.notice'), 1, compact('reason'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// no break
|
|
||||||
case 'disable':
|
|
||||||
$plugins->disable($name);
|
|
||||||
|
|
||||||
return json(trans('admin.plugins.operations.disabled', ['plugin' => $plugin->title]), 0);
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
$plugins->delete($name);
|
|
||||||
|
|
||||||
return json(trans('admin.plugins.operations.deleted'), 0);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return json(trans('admin.invalid-action'), 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return json(trans('admin.plugins.operations.not-found'), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPluginData(PluginManager $plugins)
|
|
||||||
{
|
|
||||||
return $plugins->all()
|
|
||||||
->map(function (Plugin $plugin) {
|
|
||||||
return [
|
|
||||||
'name' => $plugin->name,
|
|
||||||
'title' => trans($plugin->title),
|
|
||||||
'description' => trans($plugin->description ?? ''),
|
|
||||||
'version' => $plugin->version,
|
|
||||||
'enabled' => $plugin->isEnabled(),
|
|
||||||
'readme' => (bool) $plugin->getReadme(),
|
|
||||||
'config' => $plugin->hasConfig(),
|
|
||||||
'icon' => array_merge(
|
|
||||||
['fa' => 'plug', 'faType' => 'fas', 'bg' => 'navy'],
|
|
||||||
$plugin->getManifestAttr('enchants.icon', [])
|
|
||||||
),
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function upload(Request $request, PluginManager $manager, Unzip $unzip)
|
|
||||||
{
|
|
||||||
$request->validate(['file' => 'required|file|mimetypes:application/zip']);
|
|
||||||
|
|
||||||
$path = $request->file('file')->getPathname();
|
|
||||||
$unzip->extract($path, $manager->getPluginsDirs()->first());
|
|
||||||
|
|
||||||
return json(trans('admin.plugins.market.install-success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function wget(Request $request, PluginManager $manager, Unzip $unzip)
|
|
||||||
{
|
|
||||||
$data = $request->validate(['url' => 'required|url']);
|
|
||||||
|
|
||||||
$path = tempnam(sys_get_temp_dir(), 'wget-plugin');
|
|
||||||
$response = Http::withOptions([
|
|
||||||
'sink' => $path,
|
|
||||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
|
||||||
])->get($data['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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Report;
|
|
||||||
use App\Models\Texture;
|
|
||||||
use App\Models\User;
|
|
||||||
use Blessing\Filter;
|
|
||||||
use Blessing\Rejection;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class ReportController extends Controller
|
|
||||||
{
|
|
||||||
public function submit(Request $request, Dispatcher $dispatcher, Filter $filter)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'tid' => 'required|exists:textures',
|
|
||||||
'reason' => 'required',
|
|
||||||
]);
|
|
||||||
/** @var User */
|
|
||||||
$reporter = auth()->user();
|
|
||||||
$tid = $data['tid'];
|
|
||||||
$reason = $data['reason'];
|
|
||||||
|
|
||||||
$can = $filter->apply('user_can_report', true, [$tid, $reason, $reporter]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('report.submitting', [$tid, $reason, $reporter]);
|
|
||||||
|
|
||||||
if (Report::where('reporter', $reporter->uid)->where('tid', $tid)->count() > 0) {
|
|
||||||
return json(trans('skinlib.report.duplicate'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$score = option('reporter_score_modification', 0);
|
|
||||||
if ($score < 0 && $reporter->score < -$score) {
|
|
||||||
return json(trans('skinlib.upload.lack-score'), 1);
|
|
||||||
}
|
|
||||||
$reporter->score += $score;
|
|
||||||
$reporter->save();
|
|
||||||
|
|
||||||
$report = new Report();
|
|
||||||
$report->tid = $tid;
|
|
||||||
$report->uploader = Texture::find($tid)->uploader;
|
|
||||||
$report->reporter = $reporter->uid;
|
|
||||||
$report->reason = $reason;
|
|
||||||
$report->status = Report::PENDING;
|
|
||||||
$report->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('report.submitted', [$report]);
|
|
||||||
|
|
||||||
return json(trans('skinlib.report.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function track()
|
|
||||||
{
|
|
||||||
$reports = Report::where('reporter', auth()->id())
|
|
||||||
->orderBy('report_at', 'desc')
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
return view('user.report', ['reports' => $reports]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function manage(Request $request)
|
|
||||||
{
|
|
||||||
$q = $request->input('q');
|
|
||||||
|
|
||||||
return Report::usingSearchString($q)
|
|
||||||
->with(['texture', 'textureUploader', 'informer'])
|
|
||||||
->paginate(9);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function review(
|
|
||||||
Report $report,
|
|
||||||
Request $request,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
) {
|
|
||||||
$data = $request->validate([
|
|
||||||
'action' => ['required', Rule::in(['delete', 'ban', 'reject'])],
|
|
||||||
]);
|
|
||||||
$action = $data['action'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('report.reviewing', [$report, $action]);
|
|
||||||
|
|
||||||
if ($action == 'reject') {
|
|
||||||
if (
|
|
||||||
$report->informer
|
|
||||||
&& ($score = option('reporter_score_modification', 0)) > 0
|
|
||||||
&& $report->status == Report::PENDING
|
|
||||||
) {
|
|
||||||
$report->informer->score -= $score;
|
|
||||||
$report->informer->save();
|
|
||||||
}
|
|
||||||
$report->status = Report::REJECTED;
|
|
||||||
$report->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('report.rejected', [$report]);
|
|
||||||
|
|
||||||
return json(trans('general.op-success'), 0, ['status' => Report::REJECTED]);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
case 'delete':
|
|
||||||
/** @var Texture */
|
|
||||||
$texture = $report->texture;
|
|
||||||
if ($texture) {
|
|
||||||
$dispatcher->dispatch('texture.deleting', [$texture]);
|
|
||||||
Storage::disk('textures')->delete($texture->hash);
|
|
||||||
$texture->delete();
|
|
||||||
$dispatcher->dispatch('texture.deleted', [$texture]);
|
|
||||||
} else {
|
|
||||||
// The texture has been deleted by its uploader
|
|
||||||
// We will return the score, but will not give the informer any reward
|
|
||||||
self::returnScore($report);
|
|
||||||
$report->status = Report::RESOLVED;
|
|
||||||
$report->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('report.resolved', [$report, $action]);
|
|
||||||
|
|
||||||
return json(trans('general.texture-deleted'), 0, ['status' => Report::RESOLVED]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ban':
|
|
||||||
$uploader = User::find($report->uploader);
|
|
||||||
if (!$uploader) {
|
|
||||||
return json(trans('admin.users.operations.non-existent'), 1);
|
|
||||||
}
|
|
||||||
if (auth()->user()->permission <= $uploader->permission) {
|
|
||||||
return json(trans('admin.users.operations.no-permission'), 1);
|
|
||||||
}
|
|
||||||
$uploader->permission = User::BANNED;
|
|
||||||
$uploader->save();
|
|
||||||
$dispatcher->dispatch('user.banned', [$uploader]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
self::returnScore($report);
|
|
||||||
self::giveAward($report);
|
|
||||||
$report->status = Report::RESOLVED;
|
|
||||||
$report->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('report.resolved', [$report, $action]);
|
|
||||||
|
|
||||||
return json(trans('general.op-success'), 0, ['status' => Report::RESOLVED]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function returnScore($report)
|
|
||||||
{
|
|
||||||
if (
|
|
||||||
$report->status == Report::PENDING
|
|
||||||
&& ($score = option('reporter_score_modification', 0)) < 0
|
|
||||||
&& $report->informer
|
|
||||||
) {
|
|
||||||
$report->informer->score -= $score;
|
|
||||||
$report->informer->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function giveAward($report)
|
|
||||||
{
|
|
||||||
if ($report->status == Report::PENDING && $report->informer) {
|
|
||||||
$report->informer->score += option('reporter_reward_score', 0);
|
|
||||||
$report->informer->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Exceptions\PrettyPageException;
|
|
||||||
use App\Models\User;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Contracts\Console\Kernel as Artisan;
|
|
||||||
use Illuminate\Database\Connection;
|
|
||||||
use Illuminate\Database\DatabaseManager;
|
|
||||||
use Illuminate\Filesystem\Filesystem;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Vectorface\Whip\Whip;
|
|
||||||
|
|
||||||
class SetupController extends Controller
|
|
||||||
{
|
|
||||||
public function database(
|
|
||||||
Request $request,
|
|
||||||
Filesystem $filesystem,
|
|
||||||
Connection $connection,
|
|
||||||
DatabaseManager $manager,
|
|
||||||
) {
|
|
||||||
if ($request->isMethod('get')) {
|
|
||||||
try {
|
|
||||||
$connection->getPdo();
|
|
||||||
|
|
||||||
return redirect('setup/info');
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return view('setup.wizard.database', [
|
|
||||||
'host' => env('DB_HOST'),
|
|
||||||
'port' => env('DB_PORT'),
|
|
||||||
'username' => env('DB_USERNAME'),
|
|
||||||
'password' => env('DB_PASSWORD'),
|
|
||||||
'database' => env('DB_DATABASE'),
|
|
||||||
'prefix' => env('DB_PREFIX'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config([
|
|
||||||
'database.connections.temp.driver' => $request->input('type'),
|
|
||||||
'database.connections.temp.host' => $request->input('host'),
|
|
||||||
'database.connections.temp.port' => $request->input('port'),
|
|
||||||
'database.connections.temp.username' => $request->input('username'),
|
|
||||||
'database.connections.temp.password' => $request->input('password'),
|
|
||||||
'database.connections.temp.database' => $request->input('db'),
|
|
||||||
'database.connections.temp.prefix' => $request->input('prefix'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$manager->connection('temp')->getPdo();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$msg = $e->getMessage();
|
|
||||||
$type = Arr::get([
|
|
||||||
'mysql' => 'MySQL/MariaDB',
|
|
||||||
'sqlite' => 'SQLite',
|
|
||||||
'pgsql' => 'PostgreSQL',
|
|
||||||
], $request->input('type'), '');
|
|
||||||
|
|
||||||
throw new PrettyPageException(trans('setup.database.connection-error', compact('msg', 'type')), $e->getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = $filesystem->get(base_path('.env'));
|
|
||||||
$content = preg_replace(
|
|
||||||
'/DB_CONNECTION.+/',
|
|
||||||
'DB_CONNECTION='.$request->input('type', ''),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$content = preg_replace(
|
|
||||||
'/DB_HOST.+/',
|
|
||||||
'DB_HOST='.$request->input('host', ''),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$content = preg_replace(
|
|
||||||
'/DB_PORT.+/',
|
|
||||||
'DB_PORT='.$request->input('port', ''),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$content = preg_replace(
|
|
||||||
'/DB_DATABASE.+/',
|
|
||||||
'DB_DATABASE='.$request->input('db', ''),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$content = preg_replace(
|
|
||||||
'/DB_USERNAME.+/',
|
|
||||||
'DB_USERNAME='.$request->input('username', ''),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$content = preg_replace(
|
|
||||||
'/DB_PASSWORD.+/',
|
|
||||||
'DB_PASSWORD='.$request->input('password', ''),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$content = preg_replace(
|
|
||||||
'/DB_PREFIX.+/',
|
|
||||||
'DB_PREFIX='.$request->input('prefix', ''),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$filesystem->put(base_path('.env'), $content);
|
|
||||||
|
|
||||||
return redirect('setup/info');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function finish(Request $request, Filesystem $filesystem, Artisan $artisan)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'email' => 'required|email',
|
|
||||||
'nickname' => 'required',
|
|
||||||
'password' => 'required|min:8|max:32|confirmed',
|
|
||||||
'site_name' => 'required',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$artisan->call('passport:keys', ['--no-interaction' => true]);
|
|
||||||
|
|
||||||
// Create tables
|
|
||||||
$artisan->call('migrate', [
|
|
||||||
'--force' => true,
|
|
||||||
'--path' => [
|
|
||||||
'database/migrations',
|
|
||||||
'vendor/laravel/passport/database/migrations',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$siteUrl = url('/');
|
|
||||||
if (Str::endsWith($siteUrl, '/index.php')) {
|
|
||||||
$siteUrl = substr($siteUrl, 0, -10); // @codeCoverageIgnore
|
|
||||||
}
|
|
||||||
option([
|
|
||||||
'site_name' => $request->input('site_name'),
|
|
||||||
'site_url' => $siteUrl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$whip = new Whip();
|
|
||||||
$ip = $whip->getValidIpAddress();
|
|
||||||
|
|
||||||
// Register super admin
|
|
||||||
$user = new User();
|
|
||||||
$user->email = $data['email'];
|
|
||||||
$user->nickname = $data['nickname'];
|
|
||||||
$user->score = option('user_initial_score');
|
|
||||||
$user->avatar = 0;
|
|
||||||
$user->password = app('cipher')->hash($data['password'], config('secure.salt'));
|
|
||||||
$user->ip = $ip;
|
|
||||||
$user->permission = User::SUPER_ADMIN;
|
|
||||||
$user->register_at = Carbon::now();
|
|
||||||
$user->last_sign_at = Carbon::now()->subDay();
|
|
||||||
$user->verified = true;
|
|
||||||
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$filesystem->put(storage_path('install.lock'), '');
|
|
||||||
|
|
||||||
return view('setup.wizard.finish');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,461 +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\Filesystem\FilesystemAdapter;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\UploadedFile;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
use Intervention\Image\Facades\Image;
|
|
||||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
|
||||||
|
|
||||||
class SkinlibController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->middleware(function (Request $request, $next) {
|
|
||||||
/** @var User */
|
|
||||||
$user = $request->user();
|
|
||||||
/** @var Texture */
|
|
||||||
$texture = $request->route('texture');
|
|
||||||
|
|
||||||
if ($texture->uploader != $user->uid && !$user->isAdmin()) {
|
|
||||||
return json(trans('skinlib.no-permission'), 1)
|
|
||||||
->setStatusCode(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
})->only(['rename', 'privacy', 'type', 'delete']);
|
|
||||||
|
|
||||||
$this->middleware(function (Request $request, $next) {
|
|
||||||
/** @var User */
|
|
||||||
$user = $request->user();
|
|
||||||
/** @var Texture */
|
|
||||||
$texture = $request->route('texture');
|
|
||||||
|
|
||||||
if (!$texture->public) {
|
|
||||||
if (!Auth::check() || ($user->uid != $texture->uploader && !$user->isAdmin())) {
|
|
||||||
$statusCode = (int) option('status_code_for_private');
|
|
||||||
if ($statusCode === 404) {
|
|
||||||
abort($statusCode, trans('skinlib.show.deleted'));
|
|
||||||
} else {
|
|
||||||
abort(403, trans('skinlib.show.private'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
})->only(['show', 'info']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function library(Request $request)
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
// Available filters: skin, steve, alex, cape
|
|
||||||
$type = $request->input('filter', 'skin');
|
|
||||||
$uploader = $request->input('uploader');
|
|
||||||
$keyword = $request->input('keyword');
|
|
||||||
$sort = $request->input('sort', 'time');
|
|
||||||
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
|
|
||||||
|
|
||||||
return Texture::orderBy($sortBy, 'desc')
|
|
||||||
->when(
|
|
||||||
$type === 'skin',
|
|
||||||
fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
|
|
||||||
fn (Builder $query) => $query->where('type', $type),
|
|
||||||
)
|
|
||||||
->when($keyword, fn (Builder $query, $keyword) => $query->like('name', $keyword))
|
|
||||||
->when($uploader, fn (Builder $query, $uploader) => $query->where('uploader', $uploader))
|
|
||||||
->when($user, function (Builder $query, User $user) {
|
|
||||||
if (!$user->isAdmin()) {
|
|
||||||
// use closure-style `where` clause to lift up SQL priority
|
|
||||||
return $query->where(function (Builder $query) use ($user) {
|
|
||||||
$query
|
|
||||||
->where('public', true)
|
|
||||||
->orWhere('uploader', $user->uid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, function (Builder $query) {
|
|
||||||
// show public textures only to anonymous visitors
|
|
||||||
return $query->where('public', true);
|
|
||||||
})
|
|
||||||
->join('users', 'uid', 'uploader')
|
|
||||||
->select(['tid', 'name', 'type', 'uploader', 'public', 'likes', 'nickname'])
|
|
||||||
->paginate(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(Filter $filter, Texture $texture)
|
|
||||||
{
|
|
||||||
/** @var User */
|
|
||||||
$user = Auth::user();
|
|
||||||
/** @var FilesystemAdapter */
|
|
||||||
$disk = Storage::disk('textures');
|
|
||||||
|
|
||||||
if ($disk->missing($texture->hash)) {
|
|
||||||
if (option('auto_del_invalid_texture')) {
|
|
||||||
$texture->delete();
|
|
||||||
}
|
|
||||||
abort(404, trans('skinlib.show.deleted'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$badges = [];
|
|
||||||
$uploader = $texture->owner;
|
|
||||||
if ($uploader) {
|
|
||||||
if ($uploader->isAdmin()) {
|
|
||||||
$badges[] = ['text' => 'STAFF', 'color' => 'primary'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$badges = $filter->apply('user_badges', $badges, [$uploader]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$grid = [
|
|
||||||
'layout' => [
|
|
||||||
['md-8', 'md-4'],
|
|
||||||
],
|
|
||||||
'widgets' => [
|
|
||||||
[
|
|
||||||
['shared.previewer'],
|
|
||||||
['skinlib.widgets.show.side'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$grid = $filter->apply('grid:skinlib.show', $grid);
|
|
||||||
|
|
||||||
return view('skinlib.show')
|
|
||||||
->with('texture', $texture)
|
|
||||||
->with('grid', $grid)
|
|
||||||
->with('extra', [
|
|
||||||
'download' => (bool) option('allow_downloading_texture'),
|
|
||||||
'currentUid' => $user ? $user->uid : 0,
|
|
||||||
'admin' => $user && $user->isAdmin(),
|
|
||||||
'inCloset' => $user && $user->closet()->where('tid', $texture->tid)->count() > 0,
|
|
||||||
'uploaderExists' => (bool) $uploader,
|
|
||||||
'nickname' => optional($uploader)->nickname ?? trans('general.unexistent-user'),
|
|
||||||
'report' => intval(option('reporter_score_modification', 0)),
|
|
||||||
'badges' => $badges,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function info(Texture $texture)
|
|
||||||
{
|
|
||||||
return $texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function upload(Filter $filter)
|
|
||||||
{
|
|
||||||
$grid = [
|
|
||||||
'layout' => [
|
|
||||||
['md-6', 'md-6'],
|
|
||||||
],
|
|
||||||
'widgets' => [
|
|
||||||
[
|
|
||||||
['skinlib.widgets.upload.input'],
|
|
||||||
['shared.previewer'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$grid = $filter->apply('grid:skinlib.upload', $grid);
|
|
||||||
|
|
||||||
$converter = new GithubFlavoredMarkdownConverter();
|
|
||||||
|
|
||||||
return view('skinlib.upload')
|
|
||||||
->with('grid', $grid)
|
|
||||||
->with('extra', [
|
|
||||||
'rule' => ($regexp = option('texture_name_regexp'))
|
|
||||||
? trans('skinlib.upload.name-rule-regexp', compact('regexp'))
|
|
||||||
: trans('skinlib.upload.name-rule'),
|
|
||||||
'privacyNotice' => trans(
|
|
||||||
'skinlib.upload.private-score-notice',
|
|
||||||
['score' => option('private_score_per_storage')]
|
|
||||||
),
|
|
||||||
'score' => (int) auth()->user()->score,
|
|
||||||
'scorePublic' => (int) option('score_per_storage'),
|
|
||||||
'scorePrivate' => (int) option('private_score_per_storage'),
|
|
||||||
'closetItemCost' => (int) option('score_per_closet_item'),
|
|
||||||
'award' => (int) option('score_award_per_texture'),
|
|
||||||
'contentPolicy' => $converter->convertToHtml(option_localized('content_policy'))->getContent(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handleUpload(
|
|
||||||
Request $request,
|
|
||||||
Filter $filter,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
) {
|
|
||||||
$file = $request->file('file');
|
|
||||||
if ($file && !$file->isValid()) {
|
|
||||||
Log::error($file->getErrorMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $request->validate([
|
|
||||||
'name' => [
|
|
||||||
'required',
|
|
||||||
option('texture_name_regexp') ? 'regex:'.option('texture_name_regexp') : 'string',
|
|
||||||
],
|
|
||||||
'file' => 'required|mimes:png|max:'.option('max_upload_file_size'),
|
|
||||||
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
|
|
||||||
'public' => 'required|boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** @var UploadedFile */
|
|
||||||
$file = $filter->apply('uploaded_texture_file', $file);
|
|
||||||
|
|
||||||
$name = $data['name'];
|
|
||||||
$name = $filter->apply('uploaded_texture_name', $name, [$file]);
|
|
||||||
|
|
||||||
$can = $filter->apply('can_upload_texture', true, [$file, $name]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = $data['type'];
|
|
||||||
$size = getimagesize($file);
|
|
||||||
|
|
||||||
$maxWidth = option('max_texture_width', 8192);
|
|
||||||
if ($size[0] > $maxWidth) {
|
|
||||||
$message = trans('skinlib.upload.too-wide', [
|
|
||||||
'width' => $size[0],
|
|
||||||
'maxWidth' => $maxWidth,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return json($message, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
|
|
||||||
$message = trans('skinlib.upload.invalid-size', [
|
|
||||||
'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'),
|
|
||||||
'width' => $size[0],
|
|
||||||
'height' => $size[1],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return json($message, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ratio = $size[0] / $size[1];
|
|
||||||
if ($type == 'steve' || $type == 'alex') {
|
|
||||||
if ($ratio != 2 && $ratio != 1 || $type === 'alex' && $ratio === 2) {
|
|
||||||
$message = trans('skinlib.upload.invalid-size', [
|
|
||||||
'type' => trans('general.skin'),
|
|
||||||
'width' => $size[0],
|
|
||||||
'height' => $size[1],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return json($message, 1);
|
|
||||||
}
|
|
||||||
} elseif ($type == 'cape') {
|
|
||||||
if ($ratio != 2) {
|
|
||||||
$message = trans('skinlib.upload.invalid-size', [
|
|
||||||
'type' => trans('general.cape'),
|
|
||||||
'width' => $size[0],
|
|
||||||
'height' => $size[1],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return json($message, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$image = Image::make($file);
|
|
||||||
$imagick = $image->getCore();
|
|
||||||
$imagick->setOption('png:compression-filter', '0');
|
|
||||||
$imagick->setOption('png:compression-level', '9');
|
|
||||||
$imagick->setOption('png:compression-strategy', '0');
|
|
||||||
$imagick->setOption('png:exclude-chunk', 'all');
|
|
||||||
$imagick->stripImage();
|
|
||||||
$sanitized = $image->encode('png')->getEncoded();
|
|
||||||
|
|
||||||
$hash = hash('sha256', $image->encoded);
|
|
||||||
$hash = $filter->apply('uploaded_texture_hash', $hash, [$image]);
|
|
||||||
|
|
||||||
/** @var User */
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
$duplicated = Texture::where('hash', $hash)
|
|
||||||
->where(
|
|
||||||
fn (Builder $query) => $query->where('public', true)->orWhere('uploader', $user->uid)
|
|
||||||
)
|
|
||||||
->first();
|
|
||||||
if ($duplicated) {
|
|
||||||
// if the texture already uploaded was set to private,
|
|
||||||
// then allow to re-upload it.
|
|
||||||
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$fileSize = ceil(strlen($sanitized) / 1024);
|
|
||||||
$isPublic = is_string($data['public'])
|
|
||||||
? $data['public'] === '1'
|
|
||||||
: $data['public'];
|
|
||||||
$cost = $fileSize * (
|
|
||||||
$isPublic
|
|
||||||
? option('score_per_storage')
|
|
||||||
: option('private_score_per_storage')
|
|
||||||
);
|
|
||||||
$cost += option('score_per_closet_item');
|
|
||||||
$cost -= option('score_award_per_texture', 0);
|
|
||||||
if ($user->score < $cost) {
|
|
||||||
return json(trans('skinlib.upload.lack-score'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.uploading', [$image, $name, $hash]);
|
|
||||||
|
|
||||||
$texture = new Texture();
|
|
||||||
$texture->name = $name;
|
|
||||||
$texture->type = $type;
|
|
||||||
$texture->hash = $hash;
|
|
||||||
$texture->size = $fileSize;
|
|
||||||
$texture->public = $isPublic;
|
|
||||||
$texture->uploader = $user->uid;
|
|
||||||
$texture->likes = 1;
|
|
||||||
$texture->save();
|
|
||||||
|
|
||||||
/** @var FilesystemAdapter */
|
|
||||||
$disk = Storage::disk('textures');
|
|
||||||
if ($disk->missing($hash)) {
|
|
||||||
$disk->put($hash, $sanitized);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->score -= $cost;
|
|
||||||
$user->closet()->attach($texture->tid, ['item_name' => $name]);
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.uploaded', [$texture, $image]);
|
|
||||||
|
|
||||||
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
|
|
||||||
'tid' => $texture->tid,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter)
|
|
||||||
{
|
|
||||||
$can = $filter->apply('can_delete_texture', true, [$texture]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.deleting', [$texture]);
|
|
||||||
|
|
||||||
// check if file occupied
|
|
||||||
if (Texture::where('hash', $texture->hash)->count() === 1) {
|
|
||||||
Storage::disk('textures')->delete($texture->hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
$texture->delete();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.deleted', [$texture]);
|
|
||||||
|
|
||||||
return json(trans('skinlib.delete.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function privacy(Texture $texture, Dispatcher $dispatcher, Filter $filter)
|
|
||||||
{
|
|
||||||
$can = $filter->apply('can_update_texture_privacy', true, [$texture]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$uploader = $texture->owner;
|
|
||||||
$score_diff = $texture->size
|
|
||||||
* (option('private_score_per_storage') - option('score_per_storage'))
|
|
||||||
* ($texture->public ? -1 : 1);
|
|
||||||
if ($texture->public && option('take_back_scores_after_deletion', true)) {
|
|
||||||
$score_diff -= option('score_award_per_texture', 0);
|
|
||||||
}
|
|
||||||
if ($uploader->score + $score_diff < 0) {
|
|
||||||
return json(trans('skinlib.upload.lack-score'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$texture->public) {
|
|
||||||
$duplicated = Texture::where('hash', $texture->hash)
|
|
||||||
->where('public', true)
|
|
||||||
->first();
|
|
||||||
if ($duplicated) {
|
|
||||||
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.privacy.updating', [$texture]);
|
|
||||||
|
|
||||||
$uploader->score += $score_diff;
|
|
||||||
$uploader->save();
|
|
||||||
|
|
||||||
$texture->public = !$texture->public;
|
|
||||||
$texture->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.privacy.updated', [$texture]);
|
|
||||||
|
|
||||||
$message = trans('skinlib.privacy.success', [
|
|
||||||
'privacy' => (
|
|
||||||
$texture->public
|
|
||||||
? trans('general.public')
|
|
||||||
: trans('general.private')),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return json($message, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rename(
|
|
||||||
Request $request,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
Filter $filter,
|
|
||||||
Texture $texture,
|
|
||||||
) {
|
|
||||||
$data = $request->validate(['name' => [
|
|
||||||
'required',
|
|
||||||
option('texture_name_regexp')
|
|
||||||
? 'regex:'.option('texture_name_regexp')
|
|
||||||
: 'string',
|
|
||||||
]]);
|
|
||||||
$name = $data['name'];
|
|
||||||
|
|
||||||
$can = $filter->apply('can_update_texture_name', true, [$texture, $name]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.name.updating', [$texture, $name]);
|
|
||||||
|
|
||||||
$old = $texture->replicate();
|
|
||||||
$texture->name = $name;
|
|
||||||
$texture->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.name.updated', [$texture, $old]);
|
|
||||||
|
|
||||||
return json(trans('skinlib.rename.success', ['name' => $name]), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function type(
|
|
||||||
Request $request,
|
|
||||||
Dispatcher $dispatcher,
|
|
||||||
Filter $filter,
|
|
||||||
Texture $texture,
|
|
||||||
) {
|
|
||||||
$data = $request->validate([
|
|
||||||
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
|
|
||||||
]);
|
|
||||||
$type = $data['type'];
|
|
||||||
|
|
||||||
$can = $filter->apply('can_update_texture_type', true, [$texture, $type]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.type.updating', [$texture, $type]);
|
|
||||||
|
|
||||||
$old = $texture->replicate();
|
|
||||||
$texture->type = $type;
|
|
||||||
$texture->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('texture.type.updated', [$texture, $old]);
|
|
||||||
|
|
||||||
return json(trans('skinlib.model.success', ['model' => $type]), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Player;
|
|
||||||
use App\Models\Texture;
|
|
||||||
use App\Models\User;
|
|
||||||
use Blessing\Minecraft;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Intervention\Image\Facades\Image;
|
|
||||||
|
|
||||||
class TextureController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->middleware('cache.headers:public;max_age='.option('cache_expire_time'))
|
|
||||||
->only(['json']);
|
|
||||||
|
|
||||||
$this->middleware('cache.headers:etag;public;max_age='.option('cache_expire_time'))
|
|
||||||
->only([
|
|
||||||
'preview',
|
|
||||||
'raw',
|
|
||||||
'texture',
|
|
||||||
'avatarByPlayer',
|
|
||||||
'avatarByUser',
|
|
||||||
'avatarByTexture',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function json($player)
|
|
||||||
{
|
|
||||||
$player = Player::where('name', $player)->firstOrFail();
|
|
||||||
$isBanned = $player->user->permission === User::BANNED;
|
|
||||||
abort_if($isBanned, 403, trans('general.player-banned'));
|
|
||||||
|
|
||||||
return response()->json($player)->setLastModified($player->last_modified);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function previewByHash(Minecraft $minecraft, Request $request, $hash)
|
|
||||||
{
|
|
||||||
$texture = Texture::where('hash', $hash)->firstOrFail();
|
|
||||||
|
|
||||||
return $this->preview($minecraft, $request, $texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function preview(Minecraft $minecraft, Request $request, Texture $texture)
|
|
||||||
{
|
|
||||||
$tid = $texture->tid;
|
|
||||||
$hash = $texture->hash;
|
|
||||||
$usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
|
|
||||||
$format = $usePNG ? 'png' : 'webp';
|
|
||||||
|
|
||||||
$disk = Storage::disk('textures');
|
|
||||||
abort_if($disk->missing($hash), 404);
|
|
||||||
|
|
||||||
$height = (int) $request->query('height', 200);
|
|
||||||
$now = Carbon::now();
|
|
||||||
$response = Cache::remember(
|
|
||||||
'preview-t'.$tid."-$format",
|
|
||||||
option('enable_preview_cache') ? $now->addYear() : $now->addMinute(),
|
|
||||||
function () use ($minecraft, $disk, $texture, $hash, $height, $usePNG) {
|
|
||||||
$file = $disk->get($hash);
|
|
||||||
if ($texture->type === 'cape') {
|
|
||||||
$image = $minecraft->renderCape($file, $height);
|
|
||||||
} else {
|
|
||||||
$image = $minecraft->renderSkin($file, 12, $texture->type === 'alex');
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastModified = $disk->lastModified($hash);
|
|
||||||
|
|
||||||
// TODO: refactor
|
|
||||||
return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make($image)
|
|
||||||
->response($usePNG ? 'png' : 'webp', 100)
|
|
||||||
->setLastModified(Carbon::createFromTimestamp($lastModified));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function raw($tid)
|
|
||||||
{
|
|
||||||
abort_unless(option('allow_downloading_texture'), 403);
|
|
||||||
|
|
||||||
$texture = Texture::findOrFail($tid);
|
|
||||||
|
|
||||||
return $this->texture($texture->hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function texture(string $hash)
|
|
||||||
{
|
|
||||||
$disk = Storage::disk('textures');
|
|
||||||
abort_if($disk->missing($hash), 404);
|
|
||||||
|
|
||||||
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
|
|
||||||
|
|
||||||
return response($disk->get($hash))
|
|
||||||
->withHeaders([
|
|
||||||
'Content-Type' => 'image/png',
|
|
||||||
'Content-Length' => $disk->size($hash),
|
|
||||||
])
|
|
||||||
->setLastModified($lastModified);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function avatarByPlayer(Minecraft $minecraft, Request $request, $name)
|
|
||||||
{
|
|
||||||
$player = Player::where('name', $name)->firstOrFail();
|
|
||||||
|
|
||||||
return $this->avatar($minecraft, $request, $player->skin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function avatarByUser(Minecraft $minecraft, Request $request, $uid)
|
|
||||||
{
|
|
||||||
$texture = Texture::find(optional(User::find($uid))->avatar);
|
|
||||||
|
|
||||||
return $this->avatar($minecraft, $request, $texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function avatarByHash(Minecraft $minecraft, Request $request, $hash)
|
|
||||||
{
|
|
||||||
$texture = Texture::where('hash', $hash)->first();
|
|
||||||
|
|
||||||
return $this->avatar($minecraft, $request, $texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function avatarByTexture(Minecraft $minecraft, Request $request, $tid)
|
|
||||||
{
|
|
||||||
$texture = Texture::find($tid);
|
|
||||||
|
|
||||||
return $this->avatar($minecraft, $request, $texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function avatar(Minecraft $minecraft, Request $request, ?Texture $texture)
|
|
||||||
{
|
|
||||||
if (!empty($texture) && $texture->type !== 'steve' && $texture->type !== 'alex') {
|
|
||||||
return abort(422);
|
|
||||||
}
|
|
||||||
|
|
||||||
$size = (int) $request->query('size', 100);
|
|
||||||
$mode = $request->has('3d') ? '3d' : '2d';
|
|
||||||
$usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
|
|
||||||
$format = $usePNG ? 'png' : 'webp';
|
|
||||||
|
|
||||||
$disk = Storage::disk('textures');
|
|
||||||
if (is_null($texture) || $disk->missing($texture->hash)) {
|
|
||||||
// TODO: refactor
|
|
||||||
return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make(resource_path("misc/textures/avatar$mode.png"))
|
|
||||||
->resize($size, $size)
|
|
||||||
->response($usePNG ? 'png' : 'webp', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
$hash = $texture->hash;
|
|
||||||
$now = Carbon::now();
|
|
||||||
$response = Cache::remember(
|
|
||||||
'avatar-'.$mode.'-t'.$texture->tid.'-s'.$size."-$format",
|
|
||||||
option('enable_avatar_cache') ? $now->addYear() : $now->addMinute(),
|
|
||||||
function () use ($minecraft, $disk, $hash, $size, $mode, $usePNG) {
|
|
||||||
$file = $disk->get($hash);
|
|
||||||
if ($mode === '3d') {
|
|
||||||
$image = $minecraft->render3dAvatar($file, 25);
|
|
||||||
} else {
|
|
||||||
$image = $minecraft->render2dAvatar($file, 25);
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
|
|
||||||
|
|
||||||
// TODO: refactor
|
|
||||||
return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make($image)
|
|
||||||
->resize($size, $size)
|
|
||||||
->response($usePNG ? 'png' : 'webp', 100)
|
|
||||||
->setLastModified($lastModified);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Services\Translations\JavaScript;
|
|
||||||
use Illuminate\Foundation\Application;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Spatie\TranslationLoader\LanguageLine;
|
|
||||||
|
|
||||||
class TranslationsController extends Controller
|
|
||||||
{
|
|
||||||
public function list()
|
|
||||||
{
|
|
||||||
return LanguageLine::paginate(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(Request $request, Application $app, JavaScript $js)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'group' => 'required|string',
|
|
||||||
'key' => 'required|string',
|
|
||||||
'text' => 'required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$line = new LanguageLine();
|
|
||||||
$line->group = $data['group'];
|
|
||||||
$line->key = $data['key'];
|
|
||||||
$line->setTranslation($app->getLocale(), $data['text']);
|
|
||||||
$line->save();
|
|
||||||
|
|
||||||
if ($data['group'] === 'front-end') {
|
|
||||||
$js->resetTime($app->getLocale());
|
|
||||||
}
|
|
||||||
$request->session()->put('success', true);
|
|
||||||
|
|
||||||
return redirect('/admin/i18n');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(
|
|
||||||
Request $request,
|
|
||||||
Application $app,
|
|
||||||
JavaScript $js,
|
|
||||||
LanguageLine $line,
|
|
||||||
) {
|
|
||||||
$data = $request->validate(['text' => 'required|string']);
|
|
||||||
|
|
||||||
$line->setTranslation($app->getLocale(), $data['text']);
|
|
||||||
$line->save();
|
|
||||||
|
|
||||||
if ($line->group === 'front-end') {
|
|
||||||
$js->resetTime($app->getLocale());
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(trans('admin.i18n.updated'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(
|
|
||||||
Application $app,
|
|
||||||
JavaScript $js,
|
|
||||||
LanguageLine $line,
|
|
||||||
) {
|
|
||||||
$line->delete();
|
|
||||||
|
|
||||||
if ($line->group === 'front-end') {
|
|
||||||
$js->resetTime($app->getLocale());
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(trans('admin.i18n.deleted'), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Services\Unzip;
|
|
||||||
use Cache;
|
|
||||||
use Composer\CaBundle\CaBundle;
|
|
||||||
use Composer\Semver\Comparator;
|
|
||||||
use Illuminate\Filesystem\Filesystem;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
|
|
||||||
class UpdateController extends Controller
|
|
||||||
{
|
|
||||||
public const SPEC = 2;
|
|
||||||
|
|
||||||
public function showUpdatePage()
|
|
||||||
{
|
|
||||||
$info = $this->getUpdateInfo();
|
|
||||||
$canUpdate = $this->canUpdate(Arr::get($info, 'info'));
|
|
||||||
|
|
||||||
return view('admin.update', [
|
|
||||||
'info' => [
|
|
||||||
'latest' => Arr::get($info, 'info.latest'),
|
|
||||||
'current' => config('app.version'),
|
|
||||||
],
|
|
||||||
'error' => Arr::get($info, 'error', $canUpdate['reason']),
|
|
||||||
'can_update' => $canUpdate['can'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function download(Unzip $unzip, Filesystem $filesystem)
|
|
||||||
{
|
|
||||||
$info = $this->getUpdateInfo();
|
|
||||||
if (!$info['ok'] || !$this->canUpdate($info['info'])['can']) {
|
|
||||||
return json(trans('admin.update.info.up-to-date'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$info = $info['info'];
|
|
||||||
$path = tempnam(sys_get_temp_dir(), 'bs');
|
|
||||||
|
|
||||||
$response = Http::withOptions([
|
|
||||||
'sink' => $path,
|
|
||||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
|
||||||
])->get($info['url']);
|
|
||||||
|
|
||||||
if ($response->ok()) {
|
|
||||||
$unzip->extract($path, base_path());
|
|
||||||
|
|
||||||
// Delete options cache. This allows us to update the version.
|
|
||||||
$filesystem->delete(storage_path('options.php'));
|
|
||||||
|
|
||||||
return json(trans('admin.update.complete'), 0);
|
|
||||||
} else {
|
|
||||||
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getUpdateInfo()
|
|
||||||
{
|
|
||||||
$response = Http::withOptions([
|
|
||||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
|
||||||
])->get(config('app.update_source'));
|
|
||||||
|
|
||||||
if ($response->ok()) {
|
|
||||||
$info = $response->json();
|
|
||||||
if (Arr::get($info, 'spec') === self::SPEC) {
|
|
||||||
return ['ok' => true, 'info' => $info];
|
|
||||||
} else {
|
|
||||||
return ['ok' => false, 'error' => trans('admin.update.errors.spec')];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return ['ok' => false, 'error' => 'HTTP status code: '.$response->status()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canUpdate($info = [])
|
|
||||||
{
|
|
||||||
$php = Arr::get($info, 'php');
|
|
||||||
preg_match('/(\d+\.\d+\.\d+)/', PHP_VERSION, $matches);
|
|
||||||
$version = $matches[1];
|
|
||||||
if (Comparator::lessThan($version, $php)) {
|
|
||||||
return [
|
|
||||||
'can' => false,
|
|
||||||
'reason' => trans('admin.update.errors.php', ['version' => $php]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$can = Comparator::greaterThan(Arr::get($info, 'latest'), config('app.version'));
|
|
||||||
|
|
||||||
return ['can' => $can, 'reason' => ''];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,360 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Events\UserProfileUpdated;
|
|
||||||
use App\Mail\EmailVerification;
|
|
||||||
use App\Models\Texture;
|
|
||||||
use App\Models\User;
|
|
||||||
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\Mail;
|
|
||||||
use Illuminate\Support\Facades\Session;
|
|
||||||
use Illuminate\Support\Facades\URL;
|
|
||||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
|
||||||
|
|
||||||
class UserController extends Controller
|
|
||||||
{
|
|
||||||
public function user()
|
|
||||||
{
|
|
||||||
/** @var User */
|
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
return $user
|
|
||||||
->makeHidden(['password', 'ip', 'remember_token', 'verification_token']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function index(Filter $filter)
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
[$min, $max] = explode(',', option('sign_score'));
|
|
||||||
$scoreIntro = trans('user.score-intro.introduction', [
|
|
||||||
'initial_score' => option('user_initial_score'),
|
|
||||||
'score-from' => $min,
|
|
||||||
'score-to' => $max,
|
|
||||||
'return-score' => option('return_score')
|
|
||||||
? trans('user.score-intro.will-return-score')
|
|
||||||
: trans('user.score-intro.no-return-score'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$grid = [
|
|
||||||
'layout' => [
|
|
||||||
['md-7', 'md-5'],
|
|
||||||
],
|
|
||||||
'widgets' => [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
'user.widgets.email-verification',
|
|
||||||
'user.widgets.dashboard.usage',
|
|
||||||
],
|
|
||||||
['user.widgets.dashboard.announcement'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$grid = $filter->apply('grid:user.index', $grid);
|
|
||||||
|
|
||||||
$converter = new GithubFlavoredMarkdownConverter();
|
|
||||||
|
|
||||||
return view('user.index')->with([
|
|
||||||
'score_intro' => $scoreIntro,
|
|
||||||
'rates' => [
|
|
||||||
'storage' => option('score_per_storage'),
|
|
||||||
'player' => option('score_per_player'),
|
|
||||||
'closet' => option('score_per_closet_item'),
|
|
||||||
],
|
|
||||||
'announcement' => $converter->convertToHtml(option_localized('announcement')),
|
|
||||||
'grid' => $grid,
|
|
||||||
'extra' => ['unverified' => option('require_verification') && !$user->verified],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scoreInfo()
|
|
||||||
{
|
|
||||||
/** @var User */
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'user' => [
|
|
||||||
'score' => $user->score,
|
|
||||||
'lastSignAt' => $user->last_sign_at,
|
|
||||||
],
|
|
||||||
'rate' => [
|
|
||||||
'storage' => (int) option('score_per_storage'),
|
|
||||||
'players' => (int) option('score_per_player'),
|
|
||||||
],
|
|
||||||
'usage' => [
|
|
||||||
'players' => $user->players()->count(),
|
|
||||||
'storage' => (int) Texture::where('uploader', $user->uid)->sum('size'),
|
|
||||||
],
|
|
||||||
'signAfterZero' => (bool) option('sign_after_zero'),
|
|
||||||
'signGapTime' => (int) option('sign_gap_time'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sign(Dispatcher $dispatcher, Filter $filter)
|
|
||||||
{
|
|
||||||
/** @var User */
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
$can = $filter->apply('can_sign', true);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastSignTime = Carbon::parse($user->last_sign_at);
|
|
||||||
$remainingTime = option('sign_after_zero')
|
|
||||||
? Carbon::now()->diffInSeconds(
|
|
||||||
$lastSignTime <= Carbon::today() ? $lastSignTime : Carbon::tomorrow(),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
: Carbon::now()->diffInSeconds(
|
|
||||||
$lastSignTime->addHours((int) option('sign_gap_time')),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($remainingTime <= 0) {
|
|
||||||
[$min, $max] = explode(',', option('sign_score'));
|
|
||||||
$acquiredScore = rand((int) $min, (int) $max);
|
|
||||||
$acquiredScore = $filter->apply('sign_score', $acquiredScore);
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.sign.before', [$acquiredScore]);
|
|
||||||
|
|
||||||
$user->score += $acquiredScore;
|
|
||||||
$user->last_sign_at = Carbon::now();
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.sign.after', [$acquiredScore]);
|
|
||||||
|
|
||||||
return json(trans('user.sign-success', ['score' => $acquiredScore]), 0, [
|
|
||||||
'score' => $user->score,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return json('', 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sendVerificationEmail()
|
|
||||||
{
|
|
||||||
if (!option('require_verification')) {
|
|
||||||
return json(trans('user.verification.disabled'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limit of 60s
|
|
||||||
$remain = 60 + session('last_mail_time', 0) - time();
|
|
||||||
|
|
||||||
if ($remain > 0) {
|
|
||||||
return json(trans('user.verification.frequent-mail'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
if ($user->verified) {
|
|
||||||
return json(trans('user.verification.verified'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = URL::signedRoute('auth.verify', ['user' => $user], null, false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Mail::to($user->email)->send(new EmailVerification(url($url)));
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
report($e);
|
|
||||||
|
|
||||||
return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
Session::put('last_mail_time', time());
|
|
||||||
|
|
||||||
return json(trans('user.verification.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function profile(Filter $filter)
|
|
||||||
{
|
|
||||||
$user = Auth::user();
|
|
||||||
|
|
||||||
$grid = [
|
|
||||||
'layout' => [
|
|
||||||
['md-6', 'md-6'],
|
|
||||||
],
|
|
||||||
'widgets' => [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
'user.widgets.profile.avatar',
|
|
||||||
'user.widgets.profile.password',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'user.widgets.profile.nickname',
|
|
||||||
'user.widgets.profile.email',
|
|
||||||
'user.widgets.profile.delete-account',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$grid = $filter->apply('grid:user.profile', $grid);
|
|
||||||
|
|
||||||
return view('user.profile')
|
|
||||||
->with('user', $user)
|
|
||||||
->with('grid', $grid)
|
|
||||||
->with('site_name', option_localized('site_name'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handleProfile(Request $request, Filter $filter, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$action = $request->input('action', '');
|
|
||||||
/** @var User */
|
|
||||||
$user = Auth::user();
|
|
||||||
$addition = $request->except('action');
|
|
||||||
|
|
||||||
$can = $filter->apply('user_can_edit_profile', true, [$action, $addition]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.profile.updating', [$user, $action, $addition]);
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
case 'nickname':
|
|
||||||
$request->validate(['new_nickname' => 'required']);
|
|
||||||
|
|
||||||
$nickname = $request->input('new_nickname');
|
|
||||||
$user->nickname = $nickname;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
|
|
||||||
event(new UserProfileUpdated($action, $user));
|
|
||||||
|
|
||||||
return json(trans('user.profile.nickname.success', ['nickname' => $nickname]), 0);
|
|
||||||
|
|
||||||
case 'password':
|
|
||||||
$request->validate([
|
|
||||||
'current_password' => 'required|min:6|max:32',
|
|
||||||
'new_password' => 'required|min:8|max:32',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$user->verifyPassword($request->input('current_password'))) {
|
|
||||||
return json(trans('user.profile.password.wrong-password'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->changePassword($request->input('new_password'));
|
|
||||||
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
|
|
||||||
event(new UserProfileUpdated($action, $user));
|
|
||||||
|
|
||||||
Auth::logout();
|
|
||||||
|
|
||||||
return json(trans('user.profile.password.success'), 0);
|
|
||||||
|
|
||||||
case 'email':
|
|
||||||
$data = $request->validate([
|
|
||||||
'email' => 'required|email',
|
|
||||||
'password' => 'required|min:6|max:32',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (User::where('email', $data['email'])->count() > 0) {
|
|
||||||
return json(trans('user.profile.email.existed'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$user->verifyPassword($data['password'])) {
|
|
||||||
return json(trans('user.profile.email.wrong-password'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->email = $data['email'];
|
|
||||||
$user->verified = false;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
|
|
||||||
event(new UserProfileUpdated($action, $user));
|
|
||||||
|
|
||||||
Auth::logout();
|
|
||||||
|
|
||||||
return json(trans('user.profile.email.success'), 0);
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
$request->validate([
|
|
||||||
'password' => 'required|min:6|max:32',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($user->isAdmin()) {
|
|
||||||
return json(trans('user.profile.delete.admin'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$user->verifyPassword($request->input('password'))) {
|
|
||||||
return json(trans('user.profile.delete.wrong-password'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Auth::logout();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.deleting', [$user]);
|
|
||||||
|
|
||||||
$user->delete();
|
|
||||||
$dispatcher->dispatch('user.deleted', [$user]);
|
|
||||||
session()->flush();
|
|
||||||
|
|
||||||
return json(trans('user.profile.delete.success'), 0);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return json(trans('general.illegal-parameters'), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setAvatar(Request $request, Filter $filter, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$request->validate(['tid' => 'required|integer']);
|
|
||||||
$tid = $request->input('tid');
|
|
||||||
/** @var User */
|
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
$can = $filter->apply('user_can_update_avatar', true, [$user, $tid]);
|
|
||||||
if ($can instanceof Rejection) {
|
|
||||||
return json($can->getReason(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.avatar.updating', [$user, $tid]);
|
|
||||||
|
|
||||||
if ($tid == 0) {
|
|
||||||
$user->avatar = 0;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.avatar.updated', [$user, $tid]);
|
|
||||||
|
|
||||||
return json(trans('user.profile.avatar.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$texture = Texture::find($tid);
|
|
||||||
if ($texture) {
|
|
||||||
if ($texture->type == 'cape') {
|
|
||||||
return json(trans('user.profile.avatar.wrong-type'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!$texture->public
|
|
||||||
&& $user->uid !== $texture->uploader
|
|
||||||
&& !$user->isAdmin()
|
|
||||||
) {
|
|
||||||
return json(trans('skinlib.show.private'), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->avatar = $tid;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.avatar.updated', [$user, $tid]);
|
|
||||||
|
|
||||||
return json(trans('user.profile.avatar.success'), 0);
|
|
||||||
} else {
|
|
||||||
return json(trans('skinlib.non-existent'), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleDarkMode()
|
|
||||||
{
|
|
||||||
/** @var User */
|
|
||||||
$user = auth()->user();
|
|
||||||
$user->is_dark_mode = !$user->is_dark_mode;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
return response()->noContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class UsersManagementController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->middleware(function (Request $request, $next) {
|
|
||||||
/** @var User */
|
|
||||||
$targetUser = $request->route('user');
|
|
||||||
/** @var User */
|
|
||||||
$authUser = $request->user();
|
|
||||||
|
|
||||||
if (
|
|
||||||
$targetUser->isNot($authUser)
|
|
||||||
&& $targetUser->permission >= $authUser->permission
|
|
||||||
) {
|
|
||||||
return json(trans('admin.users.operations.no-permission'), 1)
|
|
||||||
->setStatusCode(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
})->except(['list']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$q = $request->input('q');
|
|
||||||
|
|
||||||
return User::usingSearchString($q)->paginate(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function email(User $user, Request $request, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'email' => [
|
|
||||||
'required', 'email', Rule::unique('users')->ignore($user),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
$email = $data['email'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.email.updating', [$user, $email]);
|
|
||||||
|
|
||||||
$old = $user->replicate();
|
|
||||||
$user->email = $email;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.email.updated', [$user, $old]);
|
|
||||||
|
|
||||||
return json(trans('admin.users.operations.email.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function verification(User $user, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$dispatcher->dispatch('user.verification.updating', [$user]);
|
|
||||||
|
|
||||||
$user->verified = !$user->verified;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.verification.updated', [$user]);
|
|
||||||
|
|
||||||
return json(trans('admin.users.operations.verification.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function nickname(User $user, Request $request, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'nickname' => 'required|string',
|
|
||||||
]);
|
|
||||||
$nickname = $data['nickname'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.nickname.updating', [$user, $nickname]);
|
|
||||||
|
|
||||||
$old = $user->replicate();
|
|
||||||
$user->nickname = $nickname;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.nickname.updated', [$user, $old]);
|
|
||||||
|
|
||||||
return json(trans('admin.users.operations.nickname.success', [
|
|
||||||
'new' => $request->input('nickname'),
|
|
||||||
]), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function password(User $user, Request $request, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'password' => 'required|string|min:8|max:16',
|
|
||||||
]);
|
|
||||||
$password = $data['password'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.password.updating', [$user, $password]);
|
|
||||||
|
|
||||||
$user->changePassword($password);
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.password.updated', [$user]);
|
|
||||||
|
|
||||||
return json(trans('admin.users.operations.password.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function score(User $user, Request $request, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'score' => 'required|integer',
|
|
||||||
]);
|
|
||||||
$score = (int) $data['score'];
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.score.updating', [$user, $score]);
|
|
||||||
|
|
||||||
$old = $user->replicate();
|
|
||||||
$user->score = $score;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.score.updated', [$user, $old]);
|
|
||||||
|
|
||||||
return json(trans('admin.users.operations.score.success'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function permission(User $user, Request $request, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'permission' => [
|
|
||||||
'required',
|
|
||||||
Rule::in([User::BANNED, User::NORMAL, User::ADMIN]),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
$permission = (int) $data['permission'];
|
|
||||||
|
|
||||||
if (
|
|
||||||
$permission === User::ADMIN
|
|
||||||
&& $request->user()->permission < User::SUPER_ADMIN
|
|
||||||
) {
|
|
||||||
return json(trans('admin.users.operations.no-permission'), 1)
|
|
||||||
->setStatusCode(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user->is($request->user())) {
|
|
||||||
return json(trans('admin.users.operations.no-permission'), 1)
|
|
||||||
->setStatusCode(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.permission.updating', [$user, $permission]);
|
|
||||||
|
|
||||||
$old = $user->replicate();
|
|
||||||
$user->permission = $permission;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
if ($permission === User::BANNED) {
|
|
||||||
$dispatcher->dispatch('user.banned', [$user]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.permission.updated', [$user, $old]);
|
|
||||||
|
|
||||||
return json(trans('admin.users.operations.permission'), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(User $user, Dispatcher $dispatcher)
|
|
||||||
{
|
|
||||||
$dispatcher->dispatch('user.deleting', [$user]);
|
|
||||||
|
|
||||||
$user->delete();
|
|
||||||
|
|
||||||
$dispatcher->dispatch('user.deleted', [$user]);
|
|
||||||
|
|
||||||
return json(trans('admin.users.operations.delete.success'), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
|
||||||
|
|
||||||
class Kernel extends HttpKernel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The application's global HTTP middleware stack.
|
|
||||||
*
|
|
||||||
* These middleware are run during every request to your application.
|
|
||||||
*
|
|
||||||
* @var array<int, class-string|string>
|
|
||||||
*/
|
|
||||||
protected $middleware = [
|
|
||||||
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
|
|
||||||
Middleware\ConvertEmptyStringsToNull::class,
|
|
||||||
Middleware\DetectLanguagePrefer::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's route middleware groups.
|
|
||||||
*
|
|
||||||
* @var array<string, array<int, class-string|string>>
|
|
||||||
*/
|
|
||||||
protected $middlewareGroups = [
|
|
||||||
'web' => [
|
|
||||||
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
|
|
||||||
Middleware\EnforceEverGreen::class,
|
|
||||||
Middleware\RedirectToSetup::class,
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
'api' => [
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
'authorize' => [
|
|
||||||
'auth:web',
|
|
||||||
Middleware\RejectBannedUser::class,
|
|
||||||
Middleware\EnsureEmailFilled::class,
|
|
||||||
Middleware\FireUserAuthenticated::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's middleware aliases.
|
|
||||||
*
|
|
||||||
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
|
|
||||||
*
|
|
||||||
* @var array<string, class-string|string>
|
|
||||||
*/
|
|
||||||
protected $middlewareAliases = [
|
|
||||||
'auth' => Middleware\Authenticate::class,
|
|
||||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
|
||||||
'guest' => Middleware\RedirectIfAuthenticated::class,
|
|
||||||
'role' => Middleware\CheckRole::class,
|
|
||||||
'setup' => Middleware\CheckInstallation::class,
|
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
|
||||||
'verified' => Middleware\CheckUserVerified::class,
|
|
||||||
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
|
|
||||||
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
|
||||||
|
|
||||||
class Authenticate extends Middleware
|
|
||||||
{
|
|
||||||
protected function redirectTo($request)
|
|
||||||
{
|
|
||||||
if (!$request->expectsJson()) {
|
|
||||||
session([
|
|
||||||
'last_requested_path' => $request->fullUrl(),
|
|
||||||
'msg' => trans('auth.check.anonymous'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return route('auth.login');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Filesystem\Filesystem;
|
|
||||||
|
|
||||||
class CheckInstallation
|
|
||||||
{
|
|
||||||
public function handle($request, \Closure $next)
|
|
||||||
{
|
|
||||||
$hasLock = resolve(Filesystem::class)->exists(storage_path('install.lock'));
|
|
||||||
if ($hasLock) {
|
|
||||||
return response()->view('setup.locked');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class CheckRole
|
|
||||||
{
|
|
||||||
protected $roles = [
|
|
||||||
'banned' => User::BANNED,
|
|
||||||
'normal' => User::NORMAL,
|
|
||||||
'admin' => User::ADMIN,
|
|
||||||
'super-admin' => User::SUPER_ADMIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
public function handle(Request $request, Closure $next, $role)
|
|
||||||
{
|
|
||||||
$permission = $request->user()->permission;
|
|
||||||
abort_if($permission < $this->roles[$role], 403);
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
class CheckUserVerified
|
|
||||||
{
|
|
||||||
public function handle($request, \Closure $next)
|
|
||||||
{
|
|
||||||
abort_if(option('require_verification') && !auth()->user()->verified, 403, trans('auth.check.verified'));
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull as Converter;
|
|
||||||
|
|
||||||
class ConvertEmptyStringsToNull extends Converter
|
|
||||||
{
|
|
||||||
protected $excepts = [
|
|
||||||
'admin/options',
|
|
||||||
'admin/score',
|
|
||||||
'admin/resource',
|
|
||||||
'admin/customize',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function handle($request, Closure $next)
|
|
||||||
{
|
|
||||||
if (in_array($request->path(), $this->excepts)) {
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::handle($request, $next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
|
|
||||||
class DetectLanguagePrefer
|
|
||||||
{
|
|
||||||
public function handle(Request $request, \Closure $next)
|
|
||||||
{
|
|
||||||
$locale = $request->input('lang')
|
|
||||||
?? $request->cookie('locale')
|
|
||||||
?? $request->getPreferredLanguage();
|
|
||||||
if (
|
|
||||||
($info = Arr::get(config('locales'), $locale))
|
|
||||||
&& ($alias = Arr::get($info, 'alias'))
|
|
||||||
) {
|
|
||||||
$locale = $alias;
|
|
||||||
}
|
|
||||||
$locale ?? app()->getLocale();
|
|
||||||
if (!Arr::has(config('locales'), $locale)) {
|
|
||||||
$locale = config('app.fallback_locale');
|
|
||||||
}
|
|
||||||
|
|
||||||
app()->setLocale($locale);
|
|
||||||
|
|
||||||
/** @var Response */
|
|
||||||
$response = $next($request);
|
|
||||||
if (!in_array('api', optional($request->route())->middleware() ?? [])) {
|
|
||||||
$response->cookie('locale', $locale, 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use App\Exceptions\PrettyPageException;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class EnforceEverGreen
|
|
||||||
{
|
|
||||||
public function handle($request, Closure $next)
|
|
||||||
{
|
|
||||||
$userAgent = $request->userAgent();
|
|
||||||
|
|
||||||
preg_match('/Chrome\/(\d+)/', $userAgent, $matches);
|
|
||||||
$isOldChrome = Arr::has($matches, 1) && $matches[1] < 55;
|
|
||||||
|
|
||||||
if ($isOldChrome || Str::contains($userAgent, ['Trident', 'MSIE'])) {
|
|
||||||
throw new PrettyPageException(trans('errors.http.ie'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user