Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
badcc7bbf1 | ||
|
|
2d90498d80 | ||
|
|
c83037a3f7 | ||
|
|
95b7a0781e | ||
|
|
8c337704df | ||
|
|
780275f87e | ||
|
|
0db0b8a01c | ||
|
|
be2b3cd9b3 | ||
|
|
2fa1d88c22 | ||
|
|
8575a52ec1 | ||
|
|
f5681c248d | ||
|
|
06634a727b | ||
|
|
d1712286fd | ||
|
|
46053a793e | ||
|
|
4e37fbd2b5 | ||
|
|
79bcff0d26 | ||
|
|
ca8a01aced | ||
|
|
8f55aab848 | ||
|
|
79150ddca8 | ||
|
|
9b4f05d66a | ||
|
|
f7de3c026d | ||
|
|
8aa3416260 | ||
|
|
5051e8b2f8 | ||
|
|
dafe2583fe | ||
|
|
b124c75e4b | ||
|
|
f076049b68 | ||
|
|
0b52c40661 | ||
|
|
60dab4fa5b | ||
|
|
257a9fc5b9 | ||
|
|
82e78d548d | ||
|
|
bf167639ff | ||
|
|
503cb20c83 | ||
|
|
1a84ea1506 | ||
|
|
4f5c2ffa98 | ||
|
|
3adf04ff6d | ||
|
|
5e9da1e492 | ||
|
|
89bc8d4ccc | ||
|
|
53435e27b4 | ||
|
|
be6a2fe873 | ||
|
|
375d821fbd | ||
|
|
6c7076b39e | ||
|
|
0b1ed82cf7 | ||
|
|
917617f657 | ||
|
|
971f2142d4 | ||
|
|
6a3e567010 | ||
|
|
6570e3327f | ||
|
|
4a2edb2291 | ||
|
|
bffdb151ea | ||
|
|
59c9a599fc | ||
|
|
5fa78b622f | ||
|
|
a3e65515f6 | ||
|
|
74d5a98f6d | ||
|
|
42a2cff7da | ||
|
|
9400d6f7c1 | ||
|
|
70d59642b1 | ||
|
|
7c7b8873de | ||
|
|
bb7dee63e4 | ||
|
|
f10f5868bb | ||
|
|
d2642eea92 | ||
|
|
9e579a7cf4 | ||
|
|
a53af2a328 | ||
|
|
2e1b98007e | ||
|
|
6cb200bc39 | ||
|
|
3c840aca46 | ||
|
|
5119a51012 | ||
|
|
395670dd60 | ||
|
|
1da1388079 | ||
|
|
4d8da4dce6 | ||
|
|
cb7f4d9806 | ||
|
|
32ad99f620 | ||
|
|
1f63e2d24b | ||
|
|
60a24c03b0 | ||
|
|
adb6aed94a | ||
|
|
72f6dc2bd0 | ||
|
|
b5e060980a | ||
|
|
11ac5cbfc7 | ||
|
|
2bf7fe1239 | ||
|
|
aafbc33664 | ||
|
|
cdcbde29f4 | ||
|
|
9ee1f286a7 | ||
|
|
298791a4e5 | ||
|
|
d00407b410 | ||
|
|
d30b2cb4c6 | ||
|
|
193fb75ec7 | ||
|
|
2e5249d5c5 | ||
|
|
ccf6598ddc | ||
|
|
f40947c688 | ||
|
|
7b8086b25b | ||
|
|
4904e1c2a4 | ||
|
|
0e70a88e13 | ||
|
|
e2575f8a9a | ||
|
|
459a232c26 | ||
|
|
fd5d8a06ce | ||
|
|
bcd4b059d5 | ||
|
|
3c8f0c9e22 | ||
|
|
6c66898fc9 | ||
|
|
a91d750b5c | ||
|
|
aaf701f364 | ||
|
|
d08996e509 | ||
|
|
58987edd12 | ||
|
|
7fc3d2443b | ||
|
|
83fa34eb75 | ||
|
|
c8b4124535 | ||
|
|
19e6d8575f | ||
|
|
ebcc693ab4 | ||
|
|
84f29de969 | ||
|
|
1490f202b3 | ||
|
|
0c1537446f | ||
|
|
a237037ade | ||
|
|
534224c212 | ||
|
|
8e43b185fe | ||
|
|
e0c7292d35 | ||
|
|
bcf8710019 | ||
|
|
53b5c1eee8 | ||
|
|
271c50afa3 | ||
|
|
8f86e768d0 | ||
|
|
e83722cd88 | ||
|
|
89ef69ba28 | ||
|
|
8f3088bcb3 | ||
|
|
fe5becf235 | ||
|
|
63d135071d | ||
|
|
55c7109b98 | ||
|
|
1597f0fac0 | ||
|
|
05cd77e963 | ||
|
|
5ae53acbb9 | ||
|
|
09acc6c7d8 | ||
|
|
d31e0ebc7e | ||
|
|
1eefaa104e | ||
|
|
c92f86d602 | ||
|
|
df8381b7c6 | ||
|
|
8454ee4306 | ||
|
|
60b870488c | ||
|
|
bf9342847f | ||
|
|
b047ee8fda | ||
|
|
b6fb15cc10 | ||
|
|
df00c41237 | ||
|
|
b7a4259f29 | ||
|
|
335e8ca498 | ||
|
|
34842787e3 | ||
|
|
eb62bd0b6f | ||
|
|
2a35e83939 |
19
.babelrc
Normal file
19
.babelrc
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"transform-inline-environment-variables"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
"transform-remove-console"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -9,5 +9,5 @@ indent_style = space
|
|||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{php,md,ps1,Dockerfile}]
|
||||
[*.{php,tpl,js,md,styl}]
|
||||
indent_size = 4
|
||||
|
|
|
|||
94
.env.example
94
.env.example
|
|
@ -1,45 +1,77 @@
|
|||
APP_DEBUG=false
|
||||
APP_ENV=production
|
||||
APP_FALLBACK_LOCALE=en
|
||||
###################################
|
||||
# Blessing Skin Server v3 #
|
||||
# Configuration #
|
||||
###################################
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=blessingskin
|
||||
DB_USERNAME=username
|
||||
DB_PASSWORD=secret
|
||||
DB_PREFIX=
|
||||
# 本文件中各个配置项的详细说明
|
||||
# 请访问 http://t.cn/RRZ1OWd 查看中文教程
|
||||
|
||||
# Be sure to disable debug at production environment!
|
||||
APP_ENV = production
|
||||
APP_DEBUG = false
|
||||
|
||||
# Database Configuration
|
||||
DB_CONNECTION = mysql
|
||||
DB_HOST = localhost
|
||||
DB_PORT = 3306
|
||||
DB_DATABASE = skin
|
||||
DB_USERNAME = username
|
||||
DB_PASSWORD = secret
|
||||
|
||||
# Table Prefix
|
||||
#
|
||||
# Change if you want to install multiple BS instances into one database.
|
||||
# The prefix may only contain letters, numbers, and underscores.
|
||||
#
|
||||
DB_PREFIX = null
|
||||
|
||||
# Hash Algorithm for Passwords
|
||||
#
|
||||
# Available values:
|
||||
# - BCRYPT, ARGON2I, PHP_PASSWORD_HASH
|
||||
# - PHP_PASSWORD_HASH
|
||||
# - MD5, SALTED2MD5
|
||||
# - SHA256, SALTED2SHA256
|
||||
# - SHA512, SALTED2SHA512
|
||||
#
|
||||
# New sites are *highly* recommended to use BCRYPT.
|
||||
# New sites are *highly* recommended to use PHP_PASSWORD_HASH.
|
||||
#
|
||||
PWD_METHOD=BCRYPT
|
||||
APP_KEY=
|
||||
PWD_METHOD = PHP_PASSWORD_HASH
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_FROM_NAME=
|
||||
# Salt
|
||||
#
|
||||
# Change it to any random string to secure your passwords & tokens.
|
||||
# You can run [php artisan salt:random] to generate a new salt.
|
||||
#
|
||||
SALT = bs893tnok114514tdkr1919yj810snpi
|
||||
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
# App Key
|
||||
#
|
||||
# Should be formatted as `"base64:".base64_encode(random_bytes(32))`.
|
||||
# Run [php artisan key:generate] to generate a new app key,
|
||||
# otherwise all the encrypted strings will not be safe.
|
||||
#
|
||||
APP_KEY = base64:MfnScX0W/ViN8bZtRt0P481rWP3igcOK80QstjbXUxI=
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
# Mail Configuration
|
||||
#
|
||||
# Leave MAIL_DRIVER empty to disable features involving sending emails.
|
||||
#
|
||||
MAIL_DRIVER = smtp
|
||||
MAIL_HOST = null
|
||||
MAIL_PORT = 465
|
||||
MAIL_USERNAME = null
|
||||
MAIL_PASSWORD = null
|
||||
MAIL_ENCRYPTION = ssl
|
||||
|
||||
PLUGINS_DIR=null
|
||||
PLUGINS_URL=null
|
||||
# Change below lines only if you know what they mean!
|
||||
CACHE_DRIVER = file
|
||||
SESSION_DRIVER = file
|
||||
QUEUE_DRIVER = sync
|
||||
FS_DRIVER = local
|
||||
|
||||
REDIS_HOST = 127.0.0.1
|
||||
REDIS_PASSWORD = null
|
||||
REDIS_PORT = 6379
|
||||
|
||||
PLUGINS_DIR = null
|
||||
PLUGINS_URL = null
|
||||
|
|
|
|||
129
.env.testing
129
.env.testing
|
|
@ -1,105 +1,38 @@
|
|||
APP_DEBUG=false
|
||||
APP_ENV=testing
|
||||
###################################
|
||||
# Blessing Skin Server v3 #
|
||||
# Testing Configuration #
|
||||
###################################
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=:memory:
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
DB_PREFIX=
|
||||
APP_ENV = testing
|
||||
APP_DEBUG = true
|
||||
|
||||
PWD_METHOD=BCRYPT
|
||||
BCRYPT_ROUNDS=4
|
||||
APP_KEY=base64:eVX/xzF5NhpGB2luswliFx9XSBsbbAP21wOi68X/P34=
|
||||
DB_CONNECTION = mysql
|
||||
DB_HOST = 127.0.0.1
|
||||
DB_PORT = 3306
|
||||
DB_DATABASE = test
|
||||
DB_USERNAME = root
|
||||
DB_PASSWORD = null
|
||||
DB_PREFIX = null
|
||||
|
||||
MAIL_MAILER=array
|
||||
MAIL_HOST=localhost
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=ssl
|
||||
PWD_METHOD = PHP_PASSWORD_HASH
|
||||
SALT = bs893tnok114514tdkr1919yj810snpi
|
||||
APP_KEY = base64:MfnScX0W/ViN8bZtRt0P481rWP3igcOK80QstjbXUxI=
|
||||
|
||||
CACHE_DRIVER=array
|
||||
SESSION_DRIVER=array
|
||||
QUEUE_CONNECTION=sync
|
||||
LOG_CHANNEL=null
|
||||
MAIL_DRIVER = smtp
|
||||
MAIL_HOST = localhost
|
||||
MAIL_PORT = 465
|
||||
MAIL_USERNAME = null
|
||||
MAIL_PASSWORD = null
|
||||
MAIL_ENCRYPTION = ssl
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=
|
||||
REDIS_PORT=6379
|
||||
CACHE_DRIVER = array
|
||||
SESSION_DRIVER = array
|
||||
QUEUE_DRIVER = sync
|
||||
FS_DRIVER = memory
|
||||
|
||||
PLUGINS_DIR=plugins
|
||||
PLUGINS_URL=
|
||||
REDIS_HOST = 127.0.0.1
|
||||
REDIS_PASSWORD = null
|
||||
REDIS_PORT = 6379
|
||||
|
||||
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-----"
|
||||
PLUGINS_DIR = null
|
||||
PLUGINS_URL = null
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
public/
|
||||
vendor/
|
||||
coverage/
|
||||
plugins/
|
||||
node_modules/
|
||||
*.d.ts
|
||||
resources/assets/tests/__mocks__/
|
||||
resources/assets/tests/ts-shims/
|
||||
resources/assets/tests/*.ts
|
||||
resources/assets/dist/
|
||||
resources/assets/src/vendor/
|
||||
|
|
|
|||
43
.eslintrc.js
Normal file
43
.eslintrc.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
module.exports = {
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["warn", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"no-unused-vars": "warn",
|
||||
"no-console": "off",
|
||||
"comma-style": ["warn", "last"],
|
||||
"prefer-const": "warn",
|
||||
"no-var": "error",
|
||||
"eqeqeq": "error",
|
||||
},
|
||||
"globals": {
|
||||
"url": false,
|
||||
"swal": false,
|
||||
"fetch": false,
|
||||
"trans": false,
|
||||
"logout": false,
|
||||
"toastr": false,
|
||||
"isEmpty": false,
|
||||
"showMsg": false,
|
||||
"blessing": true,
|
||||
"debounce": false,
|
||||
"showModal": false,
|
||||
"showAjaxError": false,
|
||||
"getQueryString": false,
|
||||
"TexturePreview": false,
|
||||
"handleDataTablesAjaxError": false
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"root": true,
|
||||
"env":{
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"jest": true,
|
||||
"jquery": true
|
||||
}
|
||||
};
|
||||
|
|
@ -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 }}
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
|
|
@ -1,29 +1,8 @@
|
|||
.DS_Store
|
||||
.env
|
||||
.sass-cache
|
||||
coverage
|
||||
.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
|
||||
|
|
@ -14,12 +14,6 @@
|
|||
RewriteRule (^\.|/\.) - [F]
|
||||
RewriteRule ^storage/.* - [F]
|
||||
|
||||
# Redirect trailing slashes if not a folder (behind a reverse proxy)
|
||||
RewriteCond %{HTTP:X-Forwarded-Proto} ^https$
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ "%{HTTP:X-Forwarded-Proto}://%{HTTP_HOST}%1" [L,R=301]
|
||||
|
||||
# Redirect trailing slashes if not a folder
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
|
|
@ -30,5 +24,3 @@
|
|||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
|
||||
AddType application/wasm .wasm
|
||||
|
|
@ -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);
|
||||
45
.travis.yml
Normal file
45
.travis.yml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
language: php
|
||||
|
||||
git:
|
||||
submodules: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- vendor
|
||||
- node_modules
|
||||
|
||||
env:
|
||||
global: APP_ENV=testing
|
||||
|
||||
services: mysql
|
||||
|
||||
install: composer install
|
||||
|
||||
before_script:
|
||||
- mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
|
||||
- php artisan key:random
|
||||
- php artisan salt:random
|
||||
|
||||
script: ./vendor/bin/phpunit
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.5
|
||||
- php: 5.6
|
||||
- php: 7.0
|
||||
script:
|
||||
- ./vendor/bin/phpunit --coverage-clover=coverage.xml
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash) -cF php
|
||||
- php: 7.1
|
||||
- php: 7.2
|
||||
- node_js: 8
|
||||
language: node_js
|
||||
install: yarn
|
||||
before_script:
|
||||
- echo "Testing JavaScript Code" > /dev/null
|
||||
script:
|
||||
- yarn run lint
|
||||
- yarn test:coverage
|
||||
after_success:
|
||||
- yarn run codecov
|
||||
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"]
|
||||
33
ISSUE_TEMPLATE.md
Normal file
33
ISSUE_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!-- 在提交一个新 issue 前,请先阅读以下内容: -->
|
||||
<!-- Before opening a new issue, please make sure to READ the articles below: -->
|
||||
<!-- * FAQ 常见问题: https://git.io/vAkNl -->
|
||||
<!-- * 报告问题的正确姿势:https://git.io/vAkNc -->
|
||||
|
||||
<!-- 把下面模板中的占位文字删除,并按照你的情况认真填写,谢谢 -->
|
||||
<!-- Please remove the placeholders and fill in the template according to your situation. -->
|
||||
|
||||
## The Problem 问题描述
|
||||
|
||||
简要描述一下你碰到的问题 / Briefly describe the issue you are experiencing.
|
||||
|
||||
## Environment 运行环境
|
||||
|
||||
请填写以下内容 / Please tell us about your environment.
|
||||
|
||||
- Blessing Skin 版本 / version of Blessing Skin
|
||||
- 虚拟主机还是 VPS / virtual hosting or VPS
|
||||
- PHP 版本 / version of PHP
|
||||
- 什么 Web 服务器,Apache 还是 Nginx / which web server and its version
|
||||
- 什么浏览器,出现错误时的地址栏 URL 是什么 / which browser and URL
|
||||
|
||||
## Error Message 错误信息
|
||||
|
||||
出现错误时的提示,请把它贴上来(截图或文本)/ Paste the error message or a screenshot here.
|
||||
|
||||
## Steps to Reproduce 重现步骤
|
||||
|
||||
详细描述你出错前的操作步骤 / Tell us how to reproduce this issue.
|
||||
|
||||
## Attachments 附件
|
||||
|
||||
如果有需要,请在这里附上你的日志文件(如果日志很长,请上传至某处并提供链接)/ Attach your log file here if necessary.
|
||||
687
LICENSE
687
LICENSE
|
|
@ -1,21 +1,674 @@
|
|||
MIT License
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (c) 2016-present The Blessing Skin Team
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{one line to give the program's name and a brief idea of what it does.}
|
||||
Copyright (C) {year} {name of author}
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
{project} Copyright (C) {year} {fullname}
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
|
|
|||
92
README-zh.md
92
README-zh.md
|
|
@ -1,92 +0,0 @@
|
|||
- **简体中文**
|
||||
- [English](./README.md)
|
||||
|
||||
<p align="center"><img src="https://media.githubusercontent.com/media/bs-community/logo/main/logo.png"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/bs-community/blessing-skin-server/CI?style=flat-square"></a>
|
||||
<a href="https://codecov.io/gh/bs-community/blessing-skin-server"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/bs-community/blessing-skin-server?style=flat-square"></a>
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/releases"><img alt="GitHub release (latest SemVer including pre-releases)" src="https://img.shields.io/github/v/release/bs-community/blessing-skin-server?include_prereleases&style=flat-square"></a>
|
||||
<a href="https://github.com/bs-community/blessing-skin-server/blob/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/bs-community/blessing-skin-server?style=flat-square"></a>
|
||||
<a href="https://discord.com/invite/QAsyEyt"><img alt="Discord" src="https://discord.com/api/guilds/761226550921658380/widget.png"></a>
|
||||
</p>
|
||||
|
||||
优雅的开源 Minecraft 皮肤站,现在,回应您的等待。
|
||||
|
||||
Blessing Skin 是一款能让您上传、管理和分享您的 Minecraft 皮肤和披风的 Web 应用程序。与修改游戏材质包不同的是,所有人都能在游戏中看到各自的皮肤和披风(当然,前提是玩家们要使用同一个皮肤站)。
|
||||
|
||||
Blessing Skin 是一个开源的 PHP 项目,这意味着您可以自由地在您的服务器上部署它。
|
||||
|
||||
## 特性
|
||||
|
||||
- 完整实现了一个皮肤站该有的功能
|
||||
- 支持单用户多个角色
|
||||
- 通过皮肤库来分享您的皮肤和披风!
|
||||
- 易于使用
|
||||
- 可视化的用户、角色、材质管理页面
|
||||
- 详细的站点配置页面
|
||||
- 多处 UI/UX 优化只为更好的用户体验
|
||||
- 安全
|
||||
- 支持多种安全密码 Hash 算法
|
||||
- 注册可要求 Email 验证
|
||||
- 防止恶意请求的积分系统
|
||||
- 强大的可扩展性
|
||||
- 多种多样的插件
|
||||
- 支持与 Authme/Discuz 等程序的用户数据对接(插件)
|
||||
- 支持自定义 Yggdrasil API 外置登录系统(插件)
|
||||
|
||||
## 环境要求
|
||||
|
||||
Blessing Skin 对您的服务器有一定的要求。在大多数情况下,下列所需的 PHP 扩展已经开启。
|
||||
|
||||
- 一台支持 URL 重写的主机,Nginx 或 Apache
|
||||
- PHP >= 8.1.0
|
||||
- 安装并启用如下 PHP 扩展:
|
||||
- OpenSSL >= 1.1.1 (TLS 1.3)
|
||||
- PDO
|
||||
- Mbstring
|
||||
- Tokenizer
|
||||
- GD
|
||||
- XML
|
||||
- Ctype
|
||||
- JSON
|
||||
- fileinfo
|
||||
- zip
|
||||
- Imagick
|
||||
|
||||
## 快速使用
|
||||
|
||||
请参阅 [安装指南](https://blessing.netlify.app/setup.html)。
|
||||
|
||||
## 插件系统
|
||||
|
||||
Blessing Skin 提供了强大的插件系统,您可以通过添加多种多样的插件来为您的皮肤站添加功能。
|
||||
|
||||
## 自行构建
|
||||
|
||||
详情可阅读 [这里](https://blessing.netlify.app/build.html)。
|
||||
|
||||
> 您可以订阅我们的 Telegram 频道 [Blessing Skin News](https://t.me/blessing_skin_news) 来获取最新开发动态。当有新的 Commit 被推送时,我们的机器人将会在频道内发送一条消息来提示您能否拉取最新代码,以及拉取后应该做什么。
|
||||
|
||||
## 国际化(i18n)
|
||||
|
||||
Blessing Skin 可支持多种语言,当前支持英语、简体中文和西班牙语。
|
||||
|
||||
如果您愿意将您的翻译贡献出来,欢迎参与 [我们的 Crowdin 项目](https://crowdin.com/project/blessing-skin)。
|
||||
|
||||
## 问题报告
|
||||
|
||||
请参阅 [报告问题的正确姿势](https://blessing.netlify.app/report.html)。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [用户手册](https://blessing.netlify.app/)
|
||||
- [插件开发文档](https://bs-plugin.netlify.app/)
|
||||
|
||||
## 版权
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-present The Blessing Skin Team
|
||||
|
||||
程序原作者为 [@printempw](https://printempw.github.io/),转载请注明。
|
||||
140
README.md
140
README.md
|
|
@ -1,90 +1,108 @@
|
|||
- [简体中文](./README-zh.md)
|
||||
- **English**
|
||||
- <b>简体中文</b>
|
||||
- [English](https://github.com/printempw/blessing-skin-server/blob/master/README_EN.md)
|
||||
|
||||
<p align="center"><img src="https://media.githubusercontent.com/media/bs-community/logo/main/logo.png"></p>
|
||||
<p align="center"><img src="https://img.blessing.studio/images/2017/01/01/bs-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/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>
|
||||
<a href="https://travis-ci.org/printempw/blessing-skin-server"><img src="https://api.travis-ci.org/printempw/blessing-skin-server.svg?branch=master" alt="Travis Building Status"></a>
|
||||
<a href="https://codecov.io/gh/printempw/blessing-skin-server"><img src="https://codecov.io/gh/printempw/blessing-skin-server/branch/master/graph/badge.svg" alt="Codecov" /></a>
|
||||
<a href="https://github.com/printempw/blessing-skin-server/releases"><img src="https://poser.pugx.org/printempw/blessing-skin-server/version" alt="Latest Stable Version"></a>
|
||||
<img src="https://img.shields.io/badge/PHP-5.5.9+-orange.svg" alt="PHP 5.5.9+">
|
||||
<img src="https://poser.pugx.org/printempw/blessing-skin-server/license" alt="License">
|
||||
<a href="https://twitter.com/printempw"><img src="https://img.shields.io/twitter/follow/printempw.svg?style=social&label=Follow" alt="Twitter Follow"></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!
|
||||
优雅的开源 Minecraft 皮肤站,现在,回应您的等待。
|
||||
|
||||
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 是一款能让您上传、管理和分享您的 Minecraft 皮肤和披风的 Web 应用程序。与修改游戏材质包不同的是,所有人都能在游戏中看到各自的皮肤和披风(当然,前提是玩家们要使用同一个皮肤站)。
|
||||
|
||||
Blessing Skin is an open-source project written in PHP, which means you can deploy it freely on your own web server!
|
||||
Blessing Skin 是一个开源的 PHP 项目,这意味着您可以自由地在您的服务器上部署它。这里有一个 [演示站点](http://skin.prinzeugen.net/)。
|
||||
|
||||
## Features
|
||||
特性
|
||||
-----------
|
||||
- 完整实现了一个皮肤站该有的功能
|
||||
- 支持单用户多个角色
|
||||
- 通过皮肤库来分享您的皮肤和披风!
|
||||
- 易于使用
|
||||
- 可视化的用户、角色、材质管理页面
|
||||
- 详细的站点配置页面
|
||||
- 多处 UI/UX 优化只为更好的用户体验
|
||||
- 安全
|
||||
- 支持多种安全密码 Hash 算法
|
||||
- 注册可要求 Email 验证(插件)
|
||||
- 防止恶意请求的积分系统
|
||||
- 强大的可扩展性
|
||||
- 多种多样的插件
|
||||
- 支持与 Authme/Discuz 等程序的用户数据对接
|
||||
- 支持自定义 Yggdrasil API 外置登录系统
|
||||
|
||||
- A fully functional skin hosting service
|
||||
- Multiple player names can be owned by one user on the website
|
||||
- Share your skins and capes online with skin library!
|
||||
- Easy-to-use
|
||||
- Visual page for user/player/texture management
|
||||
- Detailed option pages
|
||||
- Many tweaks for a better UI/UX
|
||||
- Security
|
||||
- Support many secure password hash algorithms
|
||||
- Email verification for registration
|
||||
- Score system for preventing evil requests
|
||||
- Incredibly extensible
|
||||
- Plenty of plugins available
|
||||
- Integration with Authme/Discuz (available as plugin)
|
||||
- Support custom Yggdrasil API authentication (available as plugin)
|
||||
环境要求
|
||||
-----------
|
||||
Blessing Skin 对您的服务器有一定的要求。_在大多数情况下,下列所需的 PHP 扩展已经开启。_
|
||||
|
||||
## Requirements
|
||||
- 一台支持 URL 重写的主机,Nginx、Apache 或 IIS
|
||||
- **PHP >= 5.5.9** [(服务器不支持?)](https://github.com/printempw/blessing-skin-server/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E)
|
||||
- 安装并启用如下 PHP 扩展:
|
||||
- OpenSSL
|
||||
- PDO
|
||||
- Mbstring
|
||||
- Tokenizer
|
||||
- GD
|
||||
|
||||
Blessing Skin has only a few system requirements. In most cases, these PHP extensions are already enabled.
|
||||
如果你使用的是 PHP 7.2,请先阅读 [Wiki - 在 PHP 7.2 上运行](https://github.com/printempw/blessing-skin-server/wiki/%E5%9C%A8-PHP-7.2-%E4%B8%8A%E8%BF%90%E8%A1%8C)。
|
||||
|
||||
- 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
|
||||
快速使用
|
||||
-----------
|
||||
请参阅 [Wiki - 快速安装向导](https://github.com/printempw/blessing-skin-server/wiki/%E5%BF%AB%E9%80%9F%E5%AE%89%E8%A3%85%E5%90%91%E5%AF%BC)。
|
||||
|
||||
## Quick Install
|
||||

|
||||
|
||||
Please read [Installation Guide](https://blessing.netlify.app/en/setup.html).
|
||||
插件系统
|
||||
------------
|
||||
|
||||
## Plugin System
|
||||
Blessing Skin 提供了强大的插件系统,您可以通过添加多种多样的插件来为您的皮肤站添加功能。
|
||||
|
||||
Blessing Skin provides an elegant and powerful plugin system, and you can attach plenty of functions and customization to your site via installing plugins.
|
||||
详情请参阅 [Wiki - 插件系统介绍](https://github.com/printempw/blessing-skin-server/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BB%8B%E7%BB%8D)。
|
||||
|
||||
## Build From Source
|
||||
自行构建
|
||||
------------
|
||||
如果你想为此项目作贡献,或者抢先尝试未发布的新功能,你应该先用 Git 上的代码部署。
|
||||
|
||||
Please refer to [Manual Build](https://blessing.netlify.app/build.html).
|
||||
**不推荐不熟悉 shell 操作以及不想折腾的用户使用。**
|
||||
|
||||
## Internationalization
|
||||
从 Git 上 clone 源码并安装依赖:
|
||||
|
||||
Blessing Skin supports multiple languages, while currently supporting English, Simplified Chinese and Spanish.
|
||||
```bash
|
||||
$ git clone https://github.com/printempw/blessing-skin-server.git
|
||||
$ composer install
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
If you are willing to contribute your translation, welcome to join [our Crowdin project](https://crowdin.com/project/blessing-skin).
|
||||
运行自动化测试(可跳过):
|
||||
|
||||
## Report Bugs
|
||||
```bash
|
||||
$ yarn test
|
||||
$ ./vendor/bin/phpunit
|
||||
```
|
||||
|
||||
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.
|
||||
```bash
|
||||
$ yarn run build
|
||||
```
|
||||
|
||||
## Related Links
|
||||
接下来请参考「快速安装向导」进行后续安装。
|
||||
|
||||
- [User Manual](https://blessing.netlify.app/en/)
|
||||
- [Plugins Development Documentation](https://bs-plugin.netlify.app/)
|
||||
问题报告
|
||||
------------
|
||||
请参阅 [Wiki - 报告问题的正确姿势](https://github.com/printempw/blessing-skin-server/wiki/%E6%8A%A5%E5%91%8A%E9%97%AE%E9%A2%98%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF)。
|
||||
|
||||
## Copyright & License
|
||||
版权
|
||||
------------
|
||||
Copyright 2016-2018 printempw and contributors.
|
||||
|
||||
MIT License
|
||||
Blessing Skin 是基于 GNU General Public License version 3 开放源代码的自由软件,你可以遵照 GPLv3 协议来修改或重新发布本程序。
|
||||
|
||||
Copyright (c) 2016-present The Blessing Skin Team
|
||||
**例外情况**:任何为 Blessing Skin 皮肤站程序开发、调用了 Blessing Skin 插件 API 的插件程序,在未使用 Blessing Skin 程序源代码的情况下,无须采用 GPLv3 协议,也不强制要求开放插件源代码。
|
||||
|
||||
程序原作者为 [@printempw](https://blessing.studio/),转载请注明。
|
||||
|
|
|
|||
131
README_EN.md
Normal file
131
README_EN.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
- [简体中文](https://github.com/printempw/blessing-skin-server/blob/master/README.md)
|
||||
- <b>English</b>
|
||||
|
||||
<p align="center"><img src="https://img.blessing.studio/images/2017/01/01/bs-logo.png"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/printempw/blessing-skin-server"><img src="https://api.travis-ci.org/printempw/blessing-skin-server.svg?branch=master" alt="Travis Building Status"></a>
|
||||
<a href="https://codecov.io/gh/printempw/blessing-skin-server"><img src="https://codecov.io/gh/printempw/blessing-skin-server/branch/master/graph/badge.svg" alt="Codecov" /></a>
|
||||
<a href="https://github.com/printempw/blessing-skin-server/releases"><img src="https://poser.pugx.org/printempw/blessing-skin-server/version" alt="Latest Stable Version"></a>
|
||||
<img src="https://img.shields.io/badge/PHP-5.5.9+-orange.svg" alt="PHP 5.5.9+">
|
||||
<img src="https://poser.pugx.org/printempw/blessing-skin-server/license" alt="License">
|
||||
<a href="https://twitter.com/printempw"><img src="https://img.shields.io/twitter/follow/printempw.svg?style=social&label=Follow" alt="Twitter Follow"></a>
|
||||
</p>
|
||||
|
||||
Are you 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! Here is a [live demo](http://skin.prinzeugen.net/).
|
||||
|
||||
Features
|
||||
-----------
|
||||
- A fully functional skin hosting service
|
||||
- Multiple player names can be owned by one user on the website
|
||||
- Share your skins and capes online with skin library!
|
||||
- Easy-to-use
|
||||
- Visual page for user/player/texture management
|
||||
- Detailed option pages
|
||||
- Many tweaks for a better UI/UX
|
||||
- Security
|
||||
- Support many secure password hash algorithms
|
||||
- Email verification for registration (available as plugin)
|
||||
- Score system for preventing evil requests
|
||||
- Incredibly extensible
|
||||
- Plenty of plugins available
|
||||
- Integration with Authme/CrazyLogin/Discuz
|
||||
- Support custom Yggdrasil API authentication
|
||||
|
||||
Requirements
|
||||
-----------
|
||||
Blessing Skin has only a few system requirements. _In most cases, these PHP extensions are already enabled._
|
||||
|
||||
- Web server with URL rewriting enabled
|
||||
- **PHP >= 5.5.9** (use v2.x branch if your server doesn't meet the requirements)
|
||||
- OpenSSL PHP Extension
|
||||
- PDO PHP Extension
|
||||
- Mbstring PHP Extension
|
||||
- Tokenizer PHP Extension
|
||||
- GD PHP Extension (for generating CAPTCHA)
|
||||
|
||||
If you are using PHP 7.2, you should also apply this [patch](https://github.com/printempw/blessing-skin-server/wiki/%E5%9C%A8-PHP-7.2-%E4%B8%8A%E8%BF%90%E8%A1%8C) after your installation. Just extract the `vendor` folder from the patch, cover the stock `vendor` with that and you are done.
|
||||
|
||||
Quick Install
|
||||
-----------
|
||||
1. Download our [latest release](https://github.com/printempw/blessing-skin-server/releases), extract to where you like to installed on.
|
||||
2. Rename `.env.example` to `.env` and configure your database information. (For windows users, just rename it to `.env.`, and the last dot will be removed automatically)
|
||||
3. For Nginx users, add [rewrite rules](#configure-the-web-server) to your Nginx configuration
|
||||
4. Navigate to `http://your-domain.com/setup` in your browser. If 404 is returned, please check whether the rewrite rules works correctly.
|
||||
5. Follow the setup wizard and your website is ready-to-go.
|
||||
|
||||
Plugin System
|
||||
------------
|
||||
|
||||
Blessing Skin provides an elegant and powerful plugin system, and you can attach plenty of functions and customization to your site via installing plugins.
|
||||
|
||||
For more information, please refer to [Wiki - Introducing plugin system](https://github.com/printempw/blessing-skin-server/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BB%8B%E7%BB%8D).
|
||||
|
||||
Developer Install
|
||||
------------
|
||||
If you'd like make some contribution on the project, please deploy it from git first.
|
||||
|
||||
**You'd better have some experience on shell operations to continue.**
|
||||
|
||||
Clone the code from GitHub and install dependencies:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/printempw/blessing-skin-server.git
|
||||
$ composer install
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
Run the tests (optional):
|
||||
|
||||
```bash
|
||||
$ yarn test
|
||||
$ ./vendor/bin/phpunit
|
||||
```
|
||||
|
||||
Build the things!
|
||||
|
||||
```bash
|
||||
$ yarn run build
|
||||
```
|
||||
|
||||
Congrats! You made it. Next, please refer to No.2 of **Quick Install** section.
|
||||
|
||||
Configure the Web Server
|
||||
------------
|
||||
For Apache (most of the virtual hosts) and IIS users, there is already a pre-configured file for you. What you need is just to enjoy!
|
||||
|
||||
For Nginx users, **please add the following rules** to your Nginx configuration file:
|
||||
|
||||
```
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ /\.env {
|
||||
deny all;
|
||||
}
|
||||
```
|
||||
|
||||
Mod Configuration
|
||||
------------
|
||||
Please refer to [Wiki - Mod Configuration](https://github.com/printempw/blessing-skin-server/wiki/%E5%A6%82%E4%BD%95%E9%85%8D%E7%BD%AE%E7%9A%AE%E8%82%A4-Mod).
|
||||
|
||||

|
||||
|
||||
Report Problems
|
||||
------------
|
||||
Read [Wiki - FAQ](https://github.com/printempw/blessing-skin-server/wiki/FAQ) 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://github.com/printempw/blessing-skin-server/wiki/%E6%8A%A5%E5%91%8A%E9%97%AE%E9%A2%98%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) before reporting a problem.
|
||||
|
||||
Copyright & License
|
||||
------------
|
||||
Copyright 2016-2018 [printempw](https://blessing.studio/) and contributors.
|
||||
|
||||
Blessing Skin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
**Exception**: Any plugin developed for Blessing Skin, is not required to adopt GPLv3 License nor release its source code, provided no source code from Blessing Skin is contained in the plugin.
|
||||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
33
app/Console/Commands/Inspire.php
Normal file
33
app/Console/Commands/Inspire.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
|
||||
class Inspire extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'inspire';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Display an inspiring quote';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
|
||||
}
|
||||
}
|
||||
74
app/Console/Commands/KeyRandomCommand.php
Normal file
74
app/Console/Commands/KeyRandomCommand.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class KeyRandomCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'key:random {--show : Display the key instead of modifying files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Set the application key';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function fire()
|
||||
{
|
||||
$key = $this->generateRandomKey();
|
||||
|
||||
if ($this->option('show')) {
|
||||
return $this->line('<comment>'.$key.'</comment>');
|
||||
}
|
||||
|
||||
// Next, we will replace the application key in the environment file so it is
|
||||
// automatically setup for this developer. This key gets generated using a
|
||||
// secure random byte generator and is later base64 encoded for storage.
|
||||
$this->setKeyInEnvironmentFile($key);
|
||||
|
||||
$this->laravel['config']['app.key'] = $key;
|
||||
|
||||
$this->info("Application key [$key] set successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application key in the environment file.
|
||||
*
|
||||
* @param string $key
|
||||
* @return void
|
||||
*/
|
||||
protected function setKeyInEnvironmentFile($key)
|
||||
{
|
||||
// Unlike Illuminate\Foundation\Console\KeyGenerateCommand,
|
||||
// I add some spaces to the replace pattern.
|
||||
file_put_contents($this->laravel->environmentFilePath(), str_replace(
|
||||
'APP_KEY = '.$this->laravel['config']['app.key'],
|
||||
'APP_KEY = '.$key,
|
||||
file_get_contents($this->laravel->environmentFilePath())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random key for the application.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateRandomKey()
|
||||
{
|
||||
return 'base64:'.base64_encode(random_bytes(
|
||||
$this->laravel['config']['app.cipher'] == 'AES-128-CBC' ? 16 : 32
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,26 @@ use Illuminate\Console\Command;
|
|||
|
||||
class SaltRandomCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'salt:random {--show : Display the salt instead of modifying files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Set the application salt';
|
||||
|
||||
public function handle()
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function fire()
|
||||
{
|
||||
$salt = $this->generateRandomSalt();
|
||||
|
||||
|
|
@ -28,7 +43,13 @@ class SaltRandomCommand extends Command
|
|||
$this->info("Application salt [$salt] set successfully.");
|
||||
}
|
||||
|
||||
protected function setKeyInEnvironmentFile(string $salt)
|
||||
/**
|
||||
* Set the application salt in the environment file.
|
||||
*
|
||||
* @param string $salt
|
||||
* @return void
|
||||
*/
|
||||
protected function setKeyInEnvironmentFile($salt)
|
||||
{
|
||||
file_put_contents($this->laravel->environmentFilePath(), str_replace(
|
||||
'SALT = '.$this->laravel['config']['secure.salt'],
|
||||
|
|
@ -37,8 +58,13 @@ class SaltRandomCommand extends Command
|
|||
));
|
||||
}
|
||||
|
||||
protected function generateRandomSalt(): string
|
||||
/**
|
||||
* Generate a random salt for the application.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateRandomSalt()
|
||||
{
|
||||
return bin2hex(resolve(\Illuminate\Contracts\Encryption\Encrypter::class)->generateKey('AES-128-CBC'));
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,17 +2,31 @@
|
|||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* The Artisan commands provided by your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
\Laravel\Passport\Console\KeysCommand::class,
|
||||
Commands\BsInstallCommand::class,
|
||||
Commands\OptionsCacheCommand::class,
|
||||
Commands\PluginDisableCommand::class,
|
||||
Commands\PluginEnableCommand::class,
|
||||
Commands\Inspire::class,
|
||||
Commands\KeyRandomCommand::class,
|
||||
Commands\SaltRandomCommand::class,
|
||||
Commands\UpdateCommand::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('inspire')
|
||||
->hourly();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
app/Events/CheckPlayerExists.php
Normal file
19
app/Events/CheckPlayerExists.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class CheckPlayerExists extends Event
|
||||
{
|
||||
public $playerName;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param string $playerName
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($playerName)
|
||||
{
|
||||
$this->playerName = $playerName;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,12 @@ class ConfigureAdminMenu extends Event
|
|||
{
|
||||
public $menu;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param array $menu
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array &$menu)
|
||||
{
|
||||
// Pass array by reference
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,12 @@ class ConfigureRoutes extends Event
|
|||
{
|
||||
public $router;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Router $router
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Router $router)
|
||||
{
|
||||
$this->router = $router;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,15 @@ class ConfigureUserMenu extends Event
|
|||
{
|
||||
public $menu;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param array $menu
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array &$menu)
|
||||
{
|
||||
// Pass array by reference
|
||||
$this->menu = &$menu;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
app/Events/EncryptUserPassword.php
Normal file
25
app/Events/EncryptUserPassword.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class EncryptUserPassword extends Event
|
||||
{
|
||||
public $user;
|
||||
|
||||
public $rawPasswd;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param string $rawPasswd
|
||||
* @param User $user
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($rawPasswd, User $user)
|
||||
{
|
||||
$this->rawPasswd = $rawPasswd;
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,4 +4,5 @@ namespace App\Events;
|
|||
|
||||
abstract class Event
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
|
|||
25
app/Events/GetAvatarPreview.php
Normal file
25
app/Events/GetAvatarPreview.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Texture;
|
||||
|
||||
class GetAvatarPreview extends Event
|
||||
{
|
||||
public $size;
|
||||
|
||||
public $texture;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Texture $texture
|
||||
* @param int $size
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Texture $texture, $size)
|
||||
{
|
||||
$this->texture = $texture;
|
||||
$this->size = $size;
|
||||
}
|
||||
}
|
||||
31
app/Events/GetPlayerJson.php
Normal file
31
app/Events/GetPlayerJson.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Player;
|
||||
|
||||
class GetPlayerJson extends Event
|
||||
{
|
||||
public $player;
|
||||
|
||||
/**
|
||||
* CSL_API = 0
|
||||
* USM_API = 1
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $apiType;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Player $player
|
||||
* @param int $apiType
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Player $player, $apiType)
|
||||
{
|
||||
$this->player = $player;
|
||||
$this->apiType = $apiType;
|
||||
}
|
||||
}
|
||||
25
app/Events/GetSkinPreview.php
Normal file
25
app/Events/GetSkinPreview.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Texture;
|
||||
|
||||
class GetSkinPreview extends Event
|
||||
{
|
||||
public $size;
|
||||
|
||||
public $texture;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Texture $texture
|
||||
* @param int $size
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Texture $texture, $size)
|
||||
{
|
||||
$this->texture = $texture;
|
||||
$this->size = $size;
|
||||
}
|
||||
}
|
||||
21
app/Events/HashingFile.php
Normal file
21
app/Events/HashingFile.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use \Illuminate\Http\UploadedFile;
|
||||
|
||||
class HashingFile extends Event
|
||||
{
|
||||
public $file;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(UploadedFile $file)
|
||||
{
|
||||
$this->file = $file;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,12 @@ class PlayerProfileUpdated extends Event
|
|||
{
|
||||
public $player;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Player $player
|
||||
* @return void
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,12 @@ class PlayerWasAdded extends Event
|
|||
{
|
||||
public $player;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Player $player
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Player $player)
|
||||
{
|
||||
$this->player = $player;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ class PlayerWasDeleted extends Event
|
|||
{
|
||||
public $playerName;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param string $playerName
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($playerName)
|
||||
{
|
||||
$this->playerName = $playerName;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ class PlayerWillBeAdded extends Event
|
|||
{
|
||||
public $playerName;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param string $playerName
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($playerName)
|
||||
{
|
||||
$this->playerName = $playerName;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ class PlayerWillBeDeleted extends Event
|
|||
{
|
||||
public $player;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Player $player
|
||||
* @return void
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,12 @@ class PluginWasDeleted extends Event
|
|||
{
|
||||
public $plugin;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Plugin $plugin
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Plugin $plugin)
|
||||
{
|
||||
$this->plugin = $plugin;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ class PluginWasDisabled extends Event
|
|||
{
|
||||
public $plugin;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Plugin $plugin
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Plugin $plugin)
|
||||
{
|
||||
$this->plugin = $plugin;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ class PluginWasEnabled extends Event
|
|||
{
|
||||
public $plugin;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Plugin $plugin
|
||||
* @return void
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,32 @@ class RenderingFooter extends Event
|
|||
{
|
||||
public $contents;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param array $contents
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array &$contents)
|
||||
{
|
||||
// Pass array by reference
|
||||
$this->contents = &$contents;
|
||||
}
|
||||
|
||||
public function addContent(string $content)
|
||||
/**
|
||||
* Add content to page footer.
|
||||
*
|
||||
* @param string $content
|
||||
* @return void
|
||||
*/
|
||||
public function addContent($content)
|
||||
{
|
||||
$this->contents[] = $content;
|
||||
if ($content) {
|
||||
if (! is_string($content)) {
|
||||
throw new \Exception("Can not add non-string content", 1);
|
||||
}
|
||||
|
||||
$this->contents[] = $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,32 @@ class RenderingHeader extends Event
|
|||
{
|
||||
public $contents;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param array $contents
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array &$contents)
|
||||
{
|
||||
// Pass array by reference
|
||||
$this->contents = &$contents;
|
||||
}
|
||||
|
||||
public function addContent(string $content)
|
||||
/**
|
||||
* Add content to page footer.
|
||||
*
|
||||
* @param string $content
|
||||
* @return void
|
||||
*/
|
||||
public function addContent($content)
|
||||
{
|
||||
$this->contents[] = $content;
|
||||
if ($content) {
|
||||
if (! is_string($content)) {
|
||||
throw new \Exception("Can not add non-string content", 1);
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,12 @@ class UserAuthenticated extends Event
|
|||
{
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param User $user
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ class UserLoggedIn extends Event
|
|||
{
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param User $user
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ class UserProfileUpdated extends Event
|
|||
public $type;
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param string $type Which type of user profile was updated.
|
||||
* @param User $user
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($type, User $user)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ class UserRegistered extends Event
|
|||
{
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param User $user
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ class UserTryToLogin extends Event
|
|||
|
||||
public $authType;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param string $identification Email or username of the user.
|
||||
* @param string $authType "email" or "username".
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($identification, $authType)
|
||||
{
|
||||
$this->identification = $identification;
|
||||
|
|
|
|||
|
|
@ -2,64 +2,160 @@
|
|||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Passport\Exceptions\MissingScopeException;
|
||||
use Throwable;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of the exception types that should not be reported.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
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,
|
||||
HttpException::class,
|
||||
ModelNotFoundException::class,
|
||||
TokenMismatchException::class,
|
||||
ValidationException::class,
|
||||
PrettyPageException::class,
|
||||
MethodNotAllowedHttpException::class,
|
||||
];
|
||||
|
||||
public function render($request, Throwable $exception)
|
||||
/**
|
||||
* Report or log an exception.
|
||||
*
|
||||
* @param Exception $e
|
||||
* @return void
|
||||
*/
|
||||
public function report(Exception $e)
|
||||
{
|
||||
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);
|
||||
parent::report($e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param Exception $e
|
||||
* @return Response
|
||||
*/
|
||||
public function render($request, Exception $e)
|
||||
{
|
||||
if ($e instanceof ModelNotFoundException) {
|
||||
$e = new NotFoundHttpException($e->getMessage(), $e);
|
||||
}
|
||||
|
||||
return parent::render($request, $exception);
|
||||
if ($e instanceof MethodNotAllowedHttpException) {
|
||||
abort(403, trans('errors.http.method-not-allowed'));
|
||||
}
|
||||
|
||||
if ($e instanceof TokenMismatchException) {
|
||||
if (request()->ajax()) {
|
||||
return json(trans('errors.http.csrf-token-mismatch'), 1);
|
||||
}
|
||||
abort(403, trans('errors.http.csrf-token-mismatch'));
|
||||
}
|
||||
|
||||
if ($e instanceof PrettyPageException) {
|
||||
return $e->showErrorPage();
|
||||
}
|
||||
|
||||
if ($e instanceof ValidationException) {
|
||||
// Quick fix for returning 422
|
||||
// @see https://prinzeugen.net/custom-responses-of-laravel-validations/
|
||||
return $e->getResponse()->setStatusCode(200);
|
||||
}
|
||||
|
||||
foreach ($this->dontReport as $type) {
|
||||
if ($e instanceof $type) {
|
||||
return parent::render($request, $e);
|
||||
} else {
|
||||
// Hide exception details if we are not in debug mode
|
||||
if (config('app.debug') && !$request->ajax()) {
|
||||
return $this->renderExceptionWithWhoops($e);
|
||||
} else {
|
||||
return $this->renderExceptionInBrief($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function convertExceptionToArray(Throwable $e)
|
||||
/**
|
||||
* Render the given HttpException.
|
||||
*
|
||||
* @param \Symfony\Component\HttpKernel\Exception\HttpException $e
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderHttpException(HttpException $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']);
|
||||
$status = $e->getStatusCode();
|
||||
$message = $e->getMessage();
|
||||
|
||||
return $trace;
|
||||
})
|
||||
->filter(function ($trace) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$isFromPlugins = !app()->runningUnitTests()
|
||||
&& Str::contains($trace['file'], resolve('plugins')->getPluginsDirs()->all());
|
||||
// Get message from exception itself > translation > standard status texts
|
||||
if (! $message) {
|
||||
if (trans()->has($transKey = "errors.http.msg-{$status}")) {
|
||||
$message = trans($transKey);
|
||||
} else {
|
||||
$message = array_get(Response::$statusTexts, $status, "Status code: $status");
|
||||
}
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
return Str::startsWith($trace['file'], 'app') || $isFromPlugins;
|
||||
})
|
||||
->values(),
|
||||
];
|
||||
if (request()->ajax()) {
|
||||
return response($message, $status, $e->getHeaders());
|
||||
}
|
||||
|
||||
if (view()->exists("errors.{$status}")) {
|
||||
return response()->view("errors.{$status}", ['exception' => $e], $status, $e->getHeaders());
|
||||
}
|
||||
|
||||
return response()->view('errors.http', [
|
||||
'title' => "HTTP {$status}",
|
||||
'message' => $message
|
||||
], $status, $e->getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception using Whoops.
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param int $code
|
||||
* @param array $headers
|
||||
* @return Response
|
||||
*/
|
||||
protected function renderExceptionWithWhoops(Exception $e, $code = 200, $headers = [])
|
||||
{
|
||||
$whoops = new \Whoops\Run;
|
||||
$handler = (request()->isMethod('GET')) ?
|
||||
new \Whoops\Handler\PrettyPageHandler : new \Whoops\Handler\PlainTextHandler;
|
||||
$whoops->pushHandler($handler);
|
||||
|
||||
return response($whoops->handleException($e), $code, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception with error messages only.
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param int $code
|
||||
* @param array $headers
|
||||
* @return Response
|
||||
*/
|
||||
protected function renderExceptionInBrief(Exception $e, $code = 200, $headers = [])
|
||||
{
|
||||
if (request()->ajax()) {
|
||||
return response($e->getMessage(), $code, $headers);
|
||||
}
|
||||
|
||||
return response()->view('errors.exception', [
|
||||
'message' => $e->getMessage()
|
||||
], $code, $headers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,25 @@ namespace App\Exceptions;
|
|||
|
||||
class PrettyPageException extends \Exception
|
||||
{
|
||||
public function render()
|
||||
/**
|
||||
* Custom error handler.
|
||||
*
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param bool $render Whether to show a error page.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($message = "Error occured.", $code = -1, $render = false)
|
||||
{
|
||||
parent::__construct($message, $code);
|
||||
|
||||
if ($render) {
|
||||
$this->showErrorPage()->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function showErrorPage()
|
||||
{
|
||||
return response()->view('errors.pretty', ['code' => $this->code, 'message' => $this->message]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,148 +2,496 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Option;
|
||||
use Schema;
|
||||
use Datatables;
|
||||
use App\Events;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\User;
|
||||
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;
|
||||
use App\Services\OptionForm;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
public function index(Filter $filter)
|
||||
public function index()
|
||||
{
|
||||
$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);
|
||||
$today = Carbon::today()->timestamp;
|
||||
|
||||
return view('admin.index', [
|
||||
'grid' => $grid,
|
||||
'sum' => [
|
||||
'users' => User::count(),
|
||||
'players' => Player::count(),
|
||||
'textures' => Texture::count(),
|
||||
'storage' => Texture::select('size')->sum('size'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
// Prepare data for the graph
|
||||
$data = [];
|
||||
$labels = [];
|
||||
|
||||
public function chartData()
|
||||
{
|
||||
$xAxis = Collection::times(31, fn ($i) => Carbon::today()->subDays(31 - $i)->isoFormat('l'));
|
||||
for ($i = 6; $i >= 0; $i--) {
|
||||
$time = Carbon::createFromTimestamp($today - $i * 86400);
|
||||
|
||||
$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()) : '';
|
||||
$labels[] = $time->format('m-d');
|
||||
$data['user_registration'][] = User::like('register_at', $time->toDateString())->count();
|
||||
$data['texture_uploads'][] = Texture::like('upload_at', $time->toDateString())->count();
|
||||
}
|
||||
|
||||
$grid = [
|
||||
'layout' => [
|
||||
['md-6', 'md-6'],
|
||||
],
|
||||
'widgets' => [
|
||||
[
|
||||
['admin.widgets.status.info'],
|
||||
['admin.widgets.status.plugins'],
|
||||
],
|
||||
$datasets = [
|
||||
[
|
||||
'label' => trans('admin.index.user-registration'),
|
||||
'backgroundColor' => 'rgba(60, 141, 188, 0.6)',
|
||||
'borderColor' => '#3c8dbc',
|
||||
'pointRadius' => 0,
|
||||
'pointBorderColor' => '#3c8dbc',
|
||||
'pointBackgroundColor' => '#3c8dbc',
|
||||
'pointHoverBackgroundColor' => '#3c8dbc',
|
||||
'pointHoverBorderColor' => '#3c8dbc',
|
||||
'data' => $data['user_registration'],
|
||||
],
|
||||
[
|
||||
'label' => trans('admin.index.texture-uploads'),
|
||||
'backgroundColor' => 'rgba(210, 214, 222, 0.6)',
|
||||
'borderColor' => '#d2d6de',
|
||||
'pointRadius' => 0,
|
||||
'pointBorderColor' => '#c1c7d1',
|
||||
'pointBackgroundColor' => '#c1c7d1',
|
||||
'pointHoverBackgroundColor' => '#c1c7d1',
|
||||
'pointHoverBorderColor' => '#c1c7d1',
|
||||
'data' => $data['texture_uploads'],
|
||||
]
|
||||
];
|
||||
$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);
|
||||
$options = [
|
||||
'tooltips' => [
|
||||
'intersect' => false,
|
||||
'mode' => 'index'
|
||||
]
|
||||
];
|
||||
|
||||
return view('admin.index', ['chartOptions' => compact('labels', 'datasets', 'options')]);
|
||||
}
|
||||
|
||||
public function customize(Request $request)
|
||||
{
|
||||
if ($request->input('action') == "color") {
|
||||
$this->validate($request, [
|
||||
'color_scheme' => 'required'
|
||||
]);
|
||||
|
||||
$color_scheme = str_replace('_', '-', $request->input('color_scheme'));
|
||||
option(['color_scheme' => $color_scheme]);
|
||||
|
||||
return json(trans('admin.customize.change-color.success'), 0);
|
||||
}
|
||||
|
||||
$homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function($form)
|
||||
{
|
||||
$form->text('home_pic_url')->hint();
|
||||
|
||||
$form->text('favicon_url')->hint()->description();
|
||||
|
||||
$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.')
|
||||
->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();
|
||||
|
||||
return view('admin.customize', ['forms' => compact('homepage', 'customJsCss')]);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$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();
|
||||
|
||||
return view('admin.score', ['forms' => compact('rate', 'sign')]);
|
||||
}
|
||||
|
||||
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 (ends_with($url, '/')) {
|
||||
$url = substr($url, 0, -1);
|
||||
}
|
||||
|
||||
if (ends_with($url, '/index.php')) {
|
||||
$url = substr($url, 0, -10);
|
||||
}
|
||||
|
||||
return $url;
|
||||
});
|
||||
|
||||
$form->checkbox('user_can_register')->label();
|
||||
$form->checkbox('register_with_player_name')->label();
|
||||
$form->checkbox('require_verification')->label();
|
||||
|
||||
$form->text('regs_per_ip');
|
||||
|
||||
$form->select('ip_get_method')
|
||||
->option('0', trans('options.general.ip_get_method.HTTP_X_FORWARDED_FOR'))
|
||||
->option('1', trans('options.general.ip_get_method.REMOTE_ADDR'))
|
||||
->hint();
|
||||
|
||||
$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->select('player_name_rule')
|
||||
->option('official', trans('options.general.player_name_rule.official'))
|
||||
->option('cjk', trans('options.general.player_name_rule.cjk'))
|
||||
->option('custom', trans('options.general.player_name_rule.custom'));
|
||||
|
||||
$form->text('custom_player_name_regexp')->hint()->placeholder();
|
||||
|
||||
$form->group('player_name_length')
|
||||
->addon(trans('options.general.player_name_length.addon1'))
|
||||
->text('player_name_length_min')
|
||||
->addon(trans('options.general.player_name_length.addon2'))
|
||||
->text('player_name_length_max')
|
||||
->addon(trans('options.general.player_name_length.addon3'));
|
||||
|
||||
$form->select('api_type')
|
||||
->option('0', 'CustomSkinLoader API')
|
||||
->option('1', 'UniversalSkinAPI');
|
||||
|
||||
$form->checkbox('auto_del_invalid_texture')->label()->hint();
|
||||
|
||||
$form->checkbox('allow_downloading_texture')->label();
|
||||
|
||||
$form->text('texture_name_regexp')->hint()->placeholder();
|
||||
|
||||
$form->textarea('comment_script')->rows(6)->description();
|
||||
|
||||
$form->checkbox('allow_sending_statistics')->label()->hint();
|
||||
|
||||
})->handle(function () {
|
||||
Option::set('site_name_'.config('app.locale'), request('site_name'));
|
||||
Option::set('site_description_'.config('app.locale'), request('site_description'));
|
||||
});
|
||||
|
||||
$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'));
|
||||
});
|
||||
|
||||
$resources = Option::form('resources', OptionForm::AUTO_DETECT, function($form)
|
||||
{
|
||||
$form->checkbox('force_ssl')->label()->hint();
|
||||
$form->checkbox('auto_detect_asset_url')->label()->description();
|
||||
$form->checkbox('return_204_when_notfound')->label()->description();
|
||||
|
||||
$form->text('cache_expire_time')->hint(OptionForm::AUTO_DETECT);
|
||||
|
||||
})->type('warning')->hint(OptionForm::AUTO_DETECT)->handle();
|
||||
|
||||
return view('admin.options')->with('forms', compact('general', 'resources', 'announ'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Manage Page of Users.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function users(Request $request)
|
||||
{
|
||||
return view('admin.users');
|
||||
}
|
||||
|
||||
public function getUserData(Request $request)
|
||||
{
|
||||
// Make it still basically functional if new columns are not ready
|
||||
$columns = array_merge([
|
||||
'uid', 'email', 'nickname', 'score', 'permission', 'register_at'
|
||||
], Schema::hasColumn('users', 'verified') ? ['verified'] : []);
|
||||
|
||||
$query = User::select($columns);
|
||||
|
||||
if ($request->has('uid')) {
|
||||
$query->where('uid', $request->get('uid'));
|
||||
}
|
||||
|
||||
return Datatables::of($query)
|
||||
->setRowId('uid')
|
||||
->editColumn('email', function ($user) {
|
||||
return $user->email ?: 'EMPTY';
|
||||
})
|
||||
->addColumn('operations', app('user.current')->getPermission())
|
||||
->addColumn('players_count', function ($user) {
|
||||
return Player::where('uid', $user->uid)->count();
|
||||
})
|
||||
->make(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Manage Page of Players.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function players(Request $request)
|
||||
{
|
||||
return view('admin.players');
|
||||
}
|
||||
|
||||
public function getPlayerData(Request $request)
|
||||
{
|
||||
$query = Player::select([
|
||||
'pid', 'uid', 'player_name', 'preference', 'tid_steve', 'tid_alex', 'tid_cape', 'last_modified'
|
||||
]);
|
||||
|
||||
if ($request->has('uid')) {
|
||||
$query->where('uid', $request->get('uid'));
|
||||
}
|
||||
|
||||
if ($request->has('pid')) {
|
||||
$query->where('pid', $request->get('pid'));
|
||||
}
|
||||
|
||||
return Datatables::of($query)->setRowId('pid')->make(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ajax request from /admin/users
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function userAjaxHandler(Request $request, UserRepository $users)
|
||||
{
|
||||
$action = $request->input('action');
|
||||
$user = $users->get($request->input('uid'));
|
||||
|
||||
if (! $user) {
|
||||
return json(trans('admin.users.operations.non-existent'), 1);
|
||||
}
|
||||
|
||||
if ($user->uid !== app('user.current')->uid) {
|
||||
if ($user->permission >= app('user.current')->permission) {
|
||||
return json(trans('admin.users.operations.no-permission'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($action == "email") {
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email'
|
||||
]);
|
||||
|
||||
if ($users->get($request->input('email'), 'email')) {
|
||||
return json(trans('admin.users.operations.email.existed', ['email' => $request->input('email')]), 1);
|
||||
}
|
||||
|
||||
$user->setEmail($request->input('email'));
|
||||
|
||||
return json(trans('admin.users.operations.email.success'), 0);
|
||||
|
||||
} elseif ($action == "verification") {
|
||||
|
||||
$user->verified = !$user->verified;
|
||||
$user->save();
|
||||
|
||||
return json(trans('admin.users.operations.verification.success'), 0);
|
||||
|
||||
} elseif ($action == "nickname") {
|
||||
$this->validate($request, [
|
||||
'nickname' => 'required|no_special_chars'
|
||||
]);
|
||||
|
||||
$user->setNickName($request->input('nickname'));
|
||||
|
||||
return json(trans('admin.users.operations.nickname.success', [
|
||||
'new' => $request->input('nickname')
|
||||
]), 0);
|
||||
|
||||
} elseif ($action == "password") {
|
||||
$this->validate($request, [
|
||||
'password' => 'required|min:8|max:16'
|
||||
]);
|
||||
|
||||
$user->changePasswd($request->input('password'));
|
||||
|
||||
return json(trans('admin.users.operations.password.success'), 0);
|
||||
|
||||
} elseif ($action == "score") {
|
||||
$this->validate($request, [
|
||||
'score' => 'required|integer'
|
||||
]);
|
||||
|
||||
$user->setScore($request->input('score'));
|
||||
|
||||
return json(trans('admin.users.operations.score.success'), 0);
|
||||
|
||||
} elseif ($action == "ban") {
|
||||
$permission = $user->getPermission() == User::BANNED ? User::NORMAL : User::BANNED;
|
||||
|
||||
$user->setPermission($permission);
|
||||
|
||||
return json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('admin.users.operations.ban.'.($permission == User::BANNED ? 'ban' : 'unban').'.success'),
|
||||
'permission' => $user->getPermission()
|
||||
]);
|
||||
|
||||
} elseif ($action == "admin") {
|
||||
$permission = $user->getPermission() == User::ADMIN ? User::NORMAL : User::ADMIN;
|
||||
|
||||
$user->setPermission($permission);
|
||||
|
||||
return json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('admin.users.operations.admin.'.($permission == User::ADMIN ? 'set' : 'unset').'.success'),
|
||||
'permission' => $user->getPermission()
|
||||
]);
|
||||
|
||||
} elseif ($action == "delete") {
|
||||
$user->delete();
|
||||
|
||||
return json(trans('admin.users.operations.delete.success'), 0);
|
||||
} else {
|
||||
return json(trans('admin.users.operations.invalid'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ajax request from /admin/players
|
||||
*/
|
||||
public function playerAjaxHandler(Request $request, UserRepository $users)
|
||||
{
|
||||
$action = $request->input('action');
|
||||
|
||||
$player = Player::find($request->input('pid'));
|
||||
|
||||
if (! $player) {
|
||||
return json(trans('general.unexistent-player'), 1);
|
||||
}
|
||||
|
||||
if ($player->user()->first()->uid !== app('user.current')->uid) {
|
||||
if ($player->user->permission >= app('user.current')->permission) {
|
||||
return json(trans('admin.players.no-permission'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($action == "preference") {
|
||||
$this->validate($request, [
|
||||
'preference' => 'required|preference'
|
||||
]);
|
||||
|
||||
$player->setPreference($request->input('preference'));
|
||||
|
||||
return json(trans('admin.players.preference.success', ['player' => $player->player_name, 'preference' => $request->input('preference')]), 0);
|
||||
|
||||
} elseif ($action == "texture") {
|
||||
$this->validate($request, [
|
||||
'model' => 'required|model',
|
||||
'tid' => 'required|integer'
|
||||
]);
|
||||
|
||||
if (! Texture::find($request->tid) && $request->tid != 0)
|
||||
return json(trans('admin.players.textures.non-existent', ['tid' => $request->tid]), 1);
|
||||
|
||||
$player->setTexture(['tid_'.$request->model => $request->tid]);
|
||||
|
||||
return json(trans('admin.players.textures.success', ['player' => $player->player_name]), 0);
|
||||
|
||||
} elseif ($action == "owner") {
|
||||
$this->validate($request, [
|
||||
'uid' => 'required|integer'
|
||||
]);
|
||||
|
||||
$user = $users->get($request->input('uid'));
|
||||
|
||||
if (! $user)
|
||||
return json(trans('admin.users.operations.non-existent'), 1);
|
||||
|
||||
$player->setOwner($request->input('uid'));
|
||||
|
||||
return json(trans('admin.players.owner.success', ['player' => $player->player_name, 'user' => $user->getNickName()]), 0);
|
||||
|
||||
} elseif ($action == "delete") {
|
||||
$player->delete();
|
||||
|
||||
return json(trans('admin.players.delete.success'), 0);
|
||||
} elseif ($action == "name") {
|
||||
$this->validate($request, [
|
||||
'name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max')
|
||||
]);
|
||||
|
||||
$player->rename($request->input('name'));
|
||||
|
||||
return json(trans('admin.players.name.success', ['player' => $player->player_name]), 0, ['name' => $player->player_name]);
|
||||
} else {
|
||||
return json(trans('admin.users.operations.invalid'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one user information
|
||||
*
|
||||
* @param string $uid
|
||||
* @param UserRepository $users
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getOneUser($uid, UserRepository $users)
|
||||
{
|
||||
$user = $users->get(intval($uid));
|
||||
if ($user) {
|
||||
return json('success', 0, ['user' => $user->makeHidden([
|
||||
'password', 'ip', 'last_sign_at', 'register_at'
|
||||
])->toArray()]);
|
||||
} else {
|
||||
return json('No such user.', 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,372 +2,339 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Log;
|
||||
use Mail;
|
||||
use View;
|
||||
use Cache;
|
||||
use Cookie;
|
||||
use Option;
|
||||
use Schema;
|
||||
use Session;
|
||||
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 App\Models\Player;
|
||||
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;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function login(Filter $filter)
|
||||
public function login()
|
||||
{
|
||||
$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'),
|
||||
],
|
||||
]);
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function handleLogin(
|
||||
Request $request,
|
||||
Rules\Captcha $captcha,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
) {
|
||||
$data = $request->validate([
|
||||
public function handleLogin(Request $request, UserRepository $users)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'identification' => 'required',
|
||||
'password' => 'required|min:6|max:32',
|
||||
'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);
|
||||
}
|
||||
$identification = $request->input('identification');
|
||||
|
||||
// Guess type of identification
|
||||
$authType = filter_var($identification, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
|
||||
$authType = (validate($identification, '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;
|
||||
}
|
||||
// Get user instance from repository.
|
||||
// If the given identification is not registered yet,
|
||||
// it will return a null value.
|
||||
$user = $users->get($identification, $authType);
|
||||
|
||||
// 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);
|
||||
$loginFailsCacheKey = sha1('login_fails_'.get_client_ip());
|
||||
$loginFails = (int) Cache::get($loginFailsCacheKey, 0);
|
||||
|
||||
if ($loginFails > 3) {
|
||||
$request->validate(['captcha' => ['required', $captcha]]);
|
||||
if (strtolower($request->input('captcha')) != strtolower(session('phrase')))
|
||||
return json(trans('auth.validation.captcha'), 1);
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
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]);
|
||||
if ($user->verifyPassword($request->input('password'))) {
|
||||
Cache::forget($loginFailsCacheKey);
|
||||
|
||||
return json(trans('auth.validation.password'), 1, [
|
||||
'login_fails' => $loginFails,
|
||||
]);
|
||||
Session::put('uid' , $user->uid);
|
||||
Session::put('token', $user->getToken());
|
||||
|
||||
// Time in minutes
|
||||
$time = $request->input('keep') == true ? 10080 : 60;
|
||||
|
||||
event(new Events\UserLoggedIn($user));
|
||||
|
||||
session()->forget('last_requested_path');
|
||||
|
||||
return json(trans('auth.login.success'), 0, [
|
||||
'token' => $user->getToken()
|
||||
]) // Set cookies
|
||||
->withCookie('uid', $user->uid, $time)
|
||||
->withCookie('token', $user->getToken(), $time);
|
||||
} else {
|
||||
// Increase the counter
|
||||
Cache::put($loginFailsCacheKey, ++$loginFails, 60);
|
||||
|
||||
return json(trans('auth.validation.password'), 1, [
|
||||
'login_fails' => $loginFails
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function logout(Dispatcher $dispatcher)
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (Session::has('uid') && Session::has('token')) {
|
||||
// Flush sessions
|
||||
Session::flush();
|
||||
|
||||
$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);
|
||||
// Delete cookies
|
||||
return json(trans('auth.logout.success'), 0)
|
||||
->withCookie(Cookie::forget('uid'))
|
||||
->withCookie(Cookie::forget('token'));
|
||||
} else {
|
||||
return json(trans('auth.logout.fail'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
if (option('user_can_register')) {
|
||||
return view('auth.register');
|
||||
} else {
|
||||
throw new PrettyPageException(trans('auth.register.close'), 7);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleRegister(Request $request, UserRepository $users)
|
||||
{
|
||||
if (! $this->checkCaptcha($request))
|
||||
return json(trans('auth.validation.captcha'), 1);
|
||||
|
||||
if (! option('user_can_register'))
|
||||
return json(trans('auth.register.close'), 7);
|
||||
|
||||
// Validate nickname or player name
|
||||
$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],
|
||||
['player_name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max')] :
|
||||
['nickname' => 'required|no_special_chars|max:255'];
|
||||
$this->validate($request, array_merge([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:8|max:32'
|
||||
], $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]);
|
||||
event(new Events\CheckPlayerExists($request->get('player_name')));
|
||||
|
||||
$player = new Player();
|
||||
$player->uid = $user->uid;
|
||||
$player->name = $playerName;
|
||||
$player->tid_skin = 0;
|
||||
if (Player::where('player_name', $request->get('player_name'))->first()) {
|
||||
return json(trans('user.player.add.repeated'), 2);
|
||||
}
|
||||
}
|
||||
|
||||
// If amount of registered accounts of IP is more than allowed amounts,
|
||||
// then reject the register.
|
||||
if (User::where('ip', get_client_ip())->count() >= option('regs_per_ip')) {
|
||||
return json(trans('auth.register.max', ['regs' => option('regs_per_ip')]), 7);
|
||||
}
|
||||
|
||||
// Register a new user.
|
||||
// If the email is already registered,
|
||||
// it will return a false value.
|
||||
$user = User::register(
|
||||
$request->get('email'),
|
||||
$request->get('password'), function($user) use ($request)
|
||||
{
|
||||
$user->ip = get_client_ip();
|
||||
$user->score = option('user_initial_score');
|
||||
$user->register_at = get_datetime_string();
|
||||
$user->last_sign_at = get_datetime_string(time() - 86400);
|
||||
$user->permission = User::NORMAL;
|
||||
$user->nickname = $request->get(
|
||||
option('register_with_player_name') ? 'player_name' : 'nickname'
|
||||
);
|
||||
});
|
||||
|
||||
if (! $user) {
|
||||
return json(trans('auth.register.registered'), 5);
|
||||
}
|
||||
|
||||
event(new Events\UserRegistered($user));
|
||||
|
||||
// Add player with chosen name
|
||||
if (option('register_with_player_name')) {
|
||||
$player = new Player;
|
||||
$player->uid = $user->uid;
|
||||
$player->player_name = $request->get('player_name');
|
||||
$player->preference = 'default';
|
||||
$player->last_modified = get_datetime_string();
|
||||
$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);
|
||||
return json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('auth.register.success'),
|
||||
'token' => $user->getToken(),
|
||||
]) // Set cookies
|
||||
->withCookie('uid', $user->uid, 60)
|
||||
->withCookie('token', $user->getToken(), 60);
|
||||
}
|
||||
|
||||
public function forgot()
|
||||
{
|
||||
if (config('mail.default') != '') {
|
||||
return view('auth.forgot', [
|
||||
'extra' => [
|
||||
'recaptcha' => option('recaptcha_sitekey'),
|
||||
'invisible' => (bool) option('recaptcha_invisible'),
|
||||
],
|
||||
]);
|
||||
if (config('mail.driver') != "") {
|
||||
return view('auth.forgot');
|
||||
} 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],
|
||||
]);
|
||||
public function handleForgot(Request $request, UserRepository $users)
|
||||
{
|
||||
if (! $this->checkCaptcha($request))
|
||||
return json(trans('auth.validation.captcha'), 1);
|
||||
|
||||
if (!config('mail.default')) {
|
||||
if (! config('mail.driver')) {
|
||||
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);
|
||||
$lastMailCacheKey = sha1('last_mail_'.get_client_ip());
|
||||
$remain = $rateLimit + Cache::get($lastMailCacheKey, 0) - time();
|
||||
|
||||
// Rate limit
|
||||
if ($remain > 0) {
|
||||
return json(trans('auth.forgot.frequent-mail'), 2);
|
||||
return json([
|
||||
'errno' => 2,
|
||||
'msg' => trans('auth.forgot.frequent-mail'),
|
||||
'remain' => $remain
|
||||
]);
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
if (!$user) {
|
||||
// Get user instance
|
||||
$user = $users->get($request->input('email'), 'email');
|
||||
|
||||
if (! $user)
|
||||
return json(trans('auth.forgot.unregistered'), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('auth.forgot.ready', [$user]);
|
||||
$uid = $user->uid;
|
||||
// Generate token for password resetting
|
||||
$token = generate_random_token();
|
||||
$url = Option::get('site_url')."/auth/reset?uid=$uid&token=$token";
|
||||
|
||||
$url = URL::temporarySignedRoute(
|
||||
'auth.reset',
|
||||
Carbon::now()->addHour(),
|
||||
['uid' => $user->uid],
|
||||
false
|
||||
);
|
||||
try {
|
||||
Mail::to($email)->send(new ForgotPassword(url($url)));
|
||||
Mail::send('mails.password-reset', compact('url'), function ($m) use ($request) {
|
||||
$site_name = option_localized('site_name');
|
||||
|
||||
$m->from(config('mail.username'), $site_name);
|
||||
$m->to($request->input('email'))->subject(trans('auth.forgot.mail.title', ['sitename' => $site_name]));
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
// Write the exception to log
|
||||
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);
|
||||
Cache::put("pwd_reset_token_$uid", $token, 60);
|
||||
Cache::put($lastMailCacheKey, time(), 60);
|
||||
|
||||
return json(trans('auth.forgot.success'), 0);
|
||||
}
|
||||
|
||||
public function reset(Request $request, $uid)
|
||||
public function reset(UserRepository $users, Request $request)
|
||||
{
|
||||
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
|
||||
// Retrieve token from cache
|
||||
$uid = $request->get('uid');
|
||||
$token = Cache::get("pwd_reset_token_$uid");
|
||||
|
||||
return view('auth.reset')->with('user', User::find($uid));
|
||||
// Get user instance from repository
|
||||
$user = $users->get($uid);
|
||||
|
||||
if (! $user) {
|
||||
return redirect('auth/forgot')->with('msg', trans('auth.reset.invalid'));
|
||||
}
|
||||
|
||||
// No token exist or token mismatch (maybe expired)
|
||||
if (is_null($token) || $token != $request->get('token')) {
|
||||
return redirect('auth/forgot')->with('msg', trans('auth.reset.expired'));
|
||||
}
|
||||
|
||||
return view('auth.reset')->with('user', $user);
|
||||
}
|
||||
|
||||
public function handleReset(Dispatcher $dispatcher, Request $request, $uid)
|
||||
public function handleReset(Request $request, UserRepository $users)
|
||||
{
|
||||
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
|
||||
|
||||
['password' => $password] = $request->validate([
|
||||
$this->validate($request, [
|
||||
'uid' => 'required|integer',
|
||||
'password' => 'required|min:8|max:32',
|
||||
'token' => 'required',
|
||||
]);
|
||||
$user = User::find($uid);
|
||||
|
||||
$dispatcher->dispatch('auth.reset.before', [$user, $password]);
|
||||
$user->changePassword($password);
|
||||
$dispatcher->dispatch('auth.reset.after', [$user, $password]);
|
||||
// Retrieve token from cache
|
||||
$uid = $request->get('uid');
|
||||
$token = Cache::get("pwd_reset_token_$uid");
|
||||
|
||||
// Get user instance from repository
|
||||
$user = $users->get($uid);
|
||||
|
||||
if (! $user) {
|
||||
return json(trans('auth.reset.invalid'), 1);
|
||||
}
|
||||
|
||||
// No token exist or token mismatch (maybe expired)
|
||||
if (is_null($token) || $token != $request->get('token')) {
|
||||
return json(trans('auth.reset.expired'), 1);
|
||||
}
|
||||
|
||||
$user->changePasswd($request->get('password'));
|
||||
|
||||
return json(trans('auth.reset.success'), 0);
|
||||
}
|
||||
|
||||
public function captcha(\Gregwar\Captcha\CaptchaBuilder $builder)
|
||||
public function verify(Request $request, UserRepository $users)
|
||||
{
|
||||
$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')) {
|
||||
if (!option('require_verification') || !Schema::hasColumn('users', 'verified')) {
|
||||
throw new PrettyPageException(trans('user.verification.disabled'), 1);
|
||||
}
|
||||
|
||||
abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid'));
|
||||
// Get user instance from repository
|
||||
$user = $users->get($request->get('uid'));
|
||||
|
||||
return view('auth.verify');
|
||||
}
|
||||
if (!$user || $user->verified) {
|
||||
throw new PrettyPageException(trans('auth.verify.invalid'), 1);
|
||||
}
|
||||
|
||||
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'));
|
||||
if ($user->verification_token != $request->get('token')) {
|
||||
throw new PrettyPageException(trans('auth.verify.expired'), 1);
|
||||
}
|
||||
|
||||
$user->verified = true;
|
||||
$user->save();
|
||||
|
||||
return redirect()->route('user.home');
|
||||
return view('auth.verify');
|
||||
}
|
||||
|
||||
public function captcha()
|
||||
{
|
||||
$builder = new \Gregwar\Captcha\CaptchaBuilder;
|
||||
$builder->build($width = 100, $height = 34);
|
||||
Session::put('phrase', $builder->getPhrase());
|
||||
|
||||
ob_start();
|
||||
$builder->output();
|
||||
$captcha = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return \Response::png($captcha);
|
||||
}
|
||||
|
||||
protected function checkCaptcha($request)
|
||||
{
|
||||
return (strtolower($request->input('captcha')) == strtolower(session('phrase')));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,197 +2,125 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Texture;
|
||||
use View;
|
||||
use Option;
|
||||
use App\Models\User;
|
||||
use Blessing\Filter;
|
||||
use Blessing\Rejection;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\Closet;
|
||||
use App\Models\Texture;
|
||||
use App\Models\ClosetModel;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
|
||||
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);
|
||||
/**
|
||||
* Instance of Closet.
|
||||
*
|
||||
* @var \App\Models\Closet
|
||||
*/
|
||||
private $closet;
|
||||
|
||||
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 __construct()
|
||||
{
|
||||
$this->closet = new Closet(session('uid'));
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('user.closet')->with('user', app('user.current'));
|
||||
}
|
||||
|
||||
public function getClosetData(Request $request)
|
||||
{
|
||||
$category = $request->input('category', 'skin');
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
$page = abs($request->input('page', 1));
|
||||
$per_page = (int) $request->input('perPage', 6);
|
||||
$q = $request->input('q', null);
|
||||
|
||||
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));
|
||||
}
|
||||
$per_page = $per_page > 0 ? $per_page : 6;
|
||||
|
||||
public function allIds()
|
||||
{
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
$items = collect();
|
||||
|
||||
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 ($q) {
|
||||
// Do search
|
||||
$items = $this->closet->getItems($category)->filter(function ($item) use ($q) {
|
||||
return stristr($item['name'], $q);
|
||||
});
|
||||
} else {
|
||||
$items = $this->closet->getItems($category);
|
||||
}
|
||||
|
||||
if ($user->score < option('score_per_closet_item')) {
|
||||
return json(trans('user.closet.add.lack-score'), 1);
|
||||
// Pagination
|
||||
$total_pages = ceil($items->count() / $per_page);
|
||||
|
||||
return response()->json([
|
||||
'category' => $category,
|
||||
'items' => $items->forPage($page, $per_page)->values(),
|
||||
'total_pages' => $total_pages
|
||||
]);
|
||||
}
|
||||
|
||||
public function add(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'tid' => 'required|integer',
|
||||
'name' => 'required|no_special_chars'
|
||||
]);
|
||||
|
||||
if (app('user.current')->getScore() < option('score_per_closet_item')) {
|
||||
return json(trans('user.closet.add.lack-score'), 7);
|
||||
}
|
||||
|
||||
$tid = $request->tid;
|
||||
$texture = Texture::find($tid);
|
||||
if (!$texture) {
|
||||
if (! Texture::find($tid)) {
|
||||
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 ($this->closet->add($tid, $request->name)) {
|
||||
$t = Texture::find($tid);
|
||||
$t->likes += 1;
|
||||
$t->save();
|
||||
|
||||
if ($user->closet()->where('tid', $request->tid)->count() > 0) {
|
||||
app('user.current')->setScore(option('score_per_closet_item'), 'minus');
|
||||
|
||||
return json(trans('user.closet.add.success', ['name' => $request->input('name')]), 0);
|
||||
} else {
|
||||
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)
|
||||
public function rename(Request $request)
|
||||
{
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
$this->validate($request, [
|
||||
'tid' => 'required|integer',
|
||||
'new_name' => 'required|no_special_chars'
|
||||
]);
|
||||
|
||||
$dispatcher->dispatch('closet.removing', [$tid, $user]);
|
||||
|
||||
$item = $user->closet()->find($tid);
|
||||
if (empty($item)) {
|
||||
if ($this->closet->rename($request->tid, $request->new_name)) {
|
||||
return json(trans('user.closet.rename.success', ['name' => $request->new_name]), 0);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
public function remove(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'tid' => 'required|integer'
|
||||
]);
|
||||
|
||||
if ($this->closet->remove($request->tid)) {
|
||||
$t = Texture::find($request->tid);
|
||||
$t->likes = $t->likes - 1;
|
||||
$t->save();
|
||||
|
||||
if (option('return_score'))
|
||||
app('user.current')->setScore(option('score_per_closet_item'), 'plus');
|
||||
|
||||
return json(trans('user.closet.remove.success'), 0);
|
||||
} else {
|
||||
return json(trans('user.closet.remove.non-existent'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,23 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Validation\Validator;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use DispatchesJobs;
|
||||
use ValidatesRequests;
|
||||
use DispatchesJobs, ValidatesRequests;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function formatValidationErrors(Validator $validator)
|
||||
{
|
||||
return [
|
||||
'errno' => 1,
|
||||
'msg' => $validator->errors()->first()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,41 +2,14 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(UserRepository $users, Request $request)
|
||||
{
|
||||
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'))
|
||||
return view('index')->with('user', $users->getCurrentUser())
|
||||
->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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,99 +2,161 @@
|
|||
|
||||
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 Datatables;
|
||||
use ZipArchive;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Composer\Semver\Comparator;
|
||||
use App\Services\PluginManager;
|
||||
|
||||
class MarketController extends Controller
|
||||
{
|
||||
public function marketData(PluginManager $manager)
|
||||
/**
|
||||
* Guzzle HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $guzzle;
|
||||
|
||||
/**
|
||||
* Default request options for Guzzle HTTP client.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guzzleConfig;
|
||||
|
||||
/**
|
||||
* Cache for plugins registry.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $registryCache;
|
||||
|
||||
public function __construct(\GuzzleHttp\Client $guzzle)
|
||||
{
|
||||
$plugins = $this->fetch()->map(function ($item) use ($manager) {
|
||||
$plugin = $manager->get($item['name']);
|
||||
$this->guzzle = $guzzle;
|
||||
$this->guzzleConfig = [
|
||||
'headers' => ['User-Agent' => config('secure.user_agent')],
|
||||
'verify' => config('secure.certificates')
|
||||
];
|
||||
}
|
||||
|
||||
public function showMarket()
|
||||
{
|
||||
return view('admin.market');
|
||||
}
|
||||
|
||||
public function getMarketData()
|
||||
{
|
||||
$plugins = collect($this->getAllAvailablePlugins())->map(function ($item) {
|
||||
$plugin = plugin($item['name']);
|
||||
$manager = app('plugins');
|
||||
|
||||
if ($plugin) {
|
||||
$item['enabled'] = $plugin->isEnabled();
|
||||
$item['installed'] = $plugin->version;
|
||||
$item['can_update'] = Comparator::greaterThan($item['version'], $item['installed']);
|
||||
$item['update_available'] = Comparator::greaterThan($item['version'], $item['installed']);
|
||||
} else {
|
||||
$item['installed'] = false;
|
||||
}
|
||||
|
||||
$requirements = Arr::get($item, 'require', []);
|
||||
$requirements = array_get($item, 'require', []);
|
||||
unset($item['require']);
|
||||
|
||||
$item['dependencies'] = [
|
||||
'all' => $requirements,
|
||||
'unsatisfied' => $manager->getUnsatisfied(new Plugin('', $item)),
|
||||
'isRequirementsSatisfied' => $manager->isRequirementsSatisfied($requirements),
|
||||
'requirements' => $requirements,
|
||||
'unsatisfiedRequirements' => $manager->getUnsatisfiedRequirements($requirements)
|
||||
];
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
return $plugins;
|
||||
return Datatables::of($plugins)->setRowId('plugin-{{ $name }}')->make(true);
|
||||
}
|
||||
|
||||
public function download(Request $request, PluginManager $manager, Unzip $unzip)
|
||||
public function checkUpdates()
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$plugins = $this->fetch();
|
||||
$metadata = $plugins->firstWhere('name', $name);
|
||||
$pluginsHaveUpdate = collect($this->getAllAvailablePlugins())->filter(function ($item) {
|
||||
$plugin = plugin($item['name']);
|
||||
return $plugin && Comparator::greaterThan($item['version'], $plugin->version);
|
||||
});
|
||||
|
||||
if (!$metadata) {
|
||||
return json([
|
||||
'available' => !$pluginsHaveUpdate->isEmpty(),
|
||||
'plugins' => array_values($pluginsHaveUpdate->all())
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(Request $request, PluginManager $manager)
|
||||
{
|
||||
$name = $request->get('name');
|
||||
$metadata = $this->getPluginMetadata($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);
|
||||
// Gather plugin distribution URL
|
||||
$url = $metadata['dist']['url'];
|
||||
$filename = array_last(explode('/', $url));
|
||||
$plugins_dir = $manager->getPluginsDir();
|
||||
$tmp_path = $plugins_dir.DIRECTORY_SEPARATOR.$filename;
|
||||
|
||||
return json(trans('admin.plugins.market.unresolved'), 1, compact('reason'));
|
||||
// Download
|
||||
try {
|
||||
$this->guzzle->request('GET', $url, array_merge($this->guzzleConfig, [
|
||||
'sink' => $tmp_path
|
||||
]));
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
return json(trans('admin.plugins.market.download-failed', ['error' => $e->getMessage()]), 2);
|
||||
}
|
||||
|
||||
$path = tempnam(sys_get_temp_dir(), $name);
|
||||
$response = Http::withOptions([
|
||||
'sink' => $path,
|
||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||
])->get($metadata['dist']['url']);
|
||||
// Check file's sha1 hash
|
||||
if (sha1_file($tmp_path) !== $metadata['dist']['shasum']) {
|
||||
@unlink($tmp_path);
|
||||
return json(trans('admin.plugins.market.shasum-failed'), 3);
|
||||
}
|
||||
|
||||
if ($response->ok()) {
|
||||
$unzip->extract($path, $manager->getPluginsDirs()->first());
|
||||
// Unzip
|
||||
$zip = new ZipArchive();
|
||||
$res = $zip->open($tmp_path);
|
||||
|
||||
return json(trans('admin.plugins.market.install-success'), 0);
|
||||
if ($res === true) {
|
||||
if ($zip->extractTo($plugins_dir) === false) {
|
||||
return json(trans('admin.plugins.market.unzip-failed', ['error' => 'Unable to extract the file.']), 4);
|
||||
}
|
||||
} else {
|
||||
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
|
||||
return json(trans('admin.plugins.market.unzip-failed', ['error' => $res]), 4);
|
||||
}
|
||||
$zip->close();
|
||||
@unlink($tmp_path);
|
||||
|
||||
return json(trans('admin.plugins.market.install-success'), 0);
|
||||
}
|
||||
|
||||
protected function fetch(): Collection
|
||||
protected function getPluginMetadata($name)
|
||||
{
|
||||
$lang = in_array(app()->getLocale(), config('plugins.locales'))
|
||||
? app()->getLocale()
|
||||
: config('app.fallback_locale');
|
||||
return collect($this->getAllAvailablePlugins())->where('name', $name)->first();
|
||||
}
|
||||
|
||||
$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));
|
||||
protected function getAllAvailablePlugins()
|
||||
{
|
||||
if (! $this->registryCache) {
|
||||
try {
|
||||
$pluginsJson = $this->guzzle->request(
|
||||
'GET', config('plugins.registry'), $this->guzzleConfig
|
||||
)->getBody();
|
||||
} catch (Exception $e) {
|
||||
throw new Exception(trans('admin.plugins.market.connection-error', [
|
||||
'error' => htmlentities($e->getMessage())
|
||||
]));
|
||||
}
|
||||
|
||||
if ($response->ok()) {
|
||||
return $response->json()['packages'];
|
||||
} else {
|
||||
throw new Exception(trans('admin.plugins.market.connection-error', ['error' => $response->status()]));
|
||||
}
|
||||
})
|
||||
->flatten(1);
|
||||
$this->registryCache = json_decode($pluginsJson, true);
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
return array_get($this->registryCache, 'packages', []);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,259 +2,183 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\PlayerWasAdded;
|
||||
use App\Events\PlayerWasDeleted;
|
||||
use App\Events\PlayerWillBeAdded;
|
||||
use App\Events\PlayerWillBeDeleted;
|
||||
use View;
|
||||
use Event;
|
||||
use Option;
|
||||
use App\Models\User;
|
||||
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;
|
||||
use App\Events\PlayerWasAdded;
|
||||
use App\Events\PlayerWasDeleted;
|
||||
use App\Events\CheckPlayerExists;
|
||||
use App\Events\PlayerWillBeAdded;
|
||||
use App\Events\PlayerWillBeDeleted;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Http\Middleware\CheckPlayerExist;
|
||||
use App\Http\Middleware\CheckPlayerOwner;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* User Instance.
|
||||
*
|
||||
* @var \App\Models\User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
return $next($request);
|
||||
}, [
|
||||
'only' => ['delete', 'rename', 'setTexture', 'clearTexture'],
|
||||
/**
|
||||
* Player Instance.
|
||||
*
|
||||
* @var \App\Models\Player
|
||||
*/
|
||||
private $player;
|
||||
|
||||
public function __construct(Request $request, UserRepository $users)
|
||||
{
|
||||
$this->user = $users->get(session('uid'));
|
||||
|
||||
if ($request->has('pid')) {
|
||||
if ($this->player = Player::find($request->pid)) {
|
||||
$this->player->checkForInvalidTextures();
|
||||
}
|
||||
}
|
||||
|
||||
$this->middleware([CheckPlayerExist::class, CheckPlayerOwner::class], [
|
||||
'only' => ['delete', 'rename', 'setTexture', 'clearTexture', 'setPreference']
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Filter $filter)
|
||||
public function index()
|
||||
{
|
||||
$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'),
|
||||
]);
|
||||
return view('user.player')->with('players', $this->user->players->toArray())->with('user', $this->user);
|
||||
}
|
||||
|
||||
public function list()
|
||||
public function add(Request $request)
|
||||
{
|
||||
return Auth::user()->players;
|
||||
}
|
||||
$this->validate($request, [
|
||||
'player_name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max')
|
||||
]);
|
||||
|
||||
public function add(Request $request, Dispatcher $dispatcher, Filter $filter)
|
||||
{
|
||||
/** @var User */
|
||||
$user = Auth::user();
|
||||
event(new CheckPlayerExists($request->input('player_name')));
|
||||
|
||||
$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 (! Player::where('player_name', $request->input('player_name'))->get()->isEmpty()) {
|
||||
return json(trans('user.player.add.repeated'), 6);
|
||||
}
|
||||
|
||||
if ($user->score < (int) option('score_per_player')) {
|
||||
if ($this->user->getScore() < Option::get('score_per_player')) {
|
||||
return json(trans('user.player.add.lack-score'), 7);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('player.adding', [$name, $user]);
|
||||
event(new PlayerWillBeAdded($name));
|
||||
event(new PlayerWillBeAdded($request->input('player_name')));
|
||||
|
||||
$player = new Player();
|
||||
$player->uid = $user->uid;
|
||||
$player->name = $name;
|
||||
$player->tid_skin = 0;
|
||||
$player->tid_cape = 0;
|
||||
$player = new Player;
|
||||
|
||||
$player->uid = $this->user->uid;
|
||||
$player->player_name = $request->input('player_name');
|
||||
$player->preference = "default";
|
||||
$player->last_modified = get_datetime_string();
|
||||
$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());
|
||||
$this->user->setScore(option('score_per_player'), 'minus');
|
||||
|
||||
return json(trans('user.player.add.success', ['name' => $request->input('player_name')]), 0);
|
||||
}
|
||||
|
||||
public function delete(
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
Player $player,
|
||||
) {
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
$playerName = $player->name;
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$playerName = $this->player->player_name;
|
||||
|
||||
$dispatcher->dispatch('player.delete.attempt', [$player, $user]);
|
||||
event(new PlayerWillBeDeleted($this->player));
|
||||
|
||||
$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();
|
||||
$this->player->delete();
|
||||
|
||||
if (option('return_score')) {
|
||||
$user->score += (int) option('score_per_player');
|
||||
$user->save();
|
||||
$this->user->setScore(Option::get('score_per_player'), 'plus');
|
||||
}
|
||||
|
||||
$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 show()
|
||||
{
|
||||
return response()->json($this->player->toArray());
|
||||
}
|
||||
|
||||
public function setTexture(
|
||||
Request $request,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
Player $player,
|
||||
) {
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
public function rename(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'new_player_name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max')
|
||||
]);
|
||||
|
||||
foreach (['skin', 'cape'] as $type) {
|
||||
$tid = $request->input($type);
|
||||
$newName = $request->input('new_player_name');
|
||||
|
||||
$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]);
|
||||
}
|
||||
if (! Player::where('player_name', $newName)->get()->isEmpty()) {
|
||||
return json(trans('user.player.rename.repeated'), 6);
|
||||
}
|
||||
|
||||
return json(trans('user.player.set.success', ['name' => $player->name]), 0, $player->toArray());
|
||||
$oldName = $this->player->player_name;
|
||||
|
||||
$this->player->rename($newName);
|
||||
|
||||
return json(trans('user.player.rename.success', ['old' => $oldName, 'new' => $newName]), 0);
|
||||
}
|
||||
|
||||
public function clearTexture(
|
||||
Request $request,
|
||||
Dispatcher $dispatcher,
|
||||
Filter $filter,
|
||||
Player $player,
|
||||
) {
|
||||
$types = $request->input('type', []);
|
||||
/**
|
||||
* A wrapper of Player::setTexture().
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function setTexture(Request $request)
|
||||
{
|
||||
foreach ($request->input('tid') as $key => $value) {
|
||||
$texture = Texture::find($value);
|
||||
|
||||
foreach (['skin', 'cape'] as $type) {
|
||||
$can = $filter->apply('can_clear_texture', true, [$player, $type]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
if (! $texture) {
|
||||
return json(trans('skinlib.un-existent'), 6);
|
||||
}
|
||||
|
||||
if ($request->has($type) || in_array($type, $types)) {
|
||||
$dispatcher->dispatch('player.texture.resetting', [$player, $type]);
|
||||
$fieldName = "tid_{$texture->type}";
|
||||
|
||||
$field = "tid_$type";
|
||||
$player->$field = 0;
|
||||
$player->save();
|
||||
|
||||
$dispatcher->dispatch('player.texture.reset', [$player, $type]);
|
||||
}
|
||||
$this->player->setTexture([$fieldName => $value]);
|
||||
}
|
||||
|
||||
return json(trans('user.player.clear.success', ['name' => $player->name]), 0, $player->toArray());
|
||||
// When user applies an alex skin to a newly added player,
|
||||
// which textures and model preference are all default value,
|
||||
// we will automatically set the player's model preference to "slim".
|
||||
if ($this->player->preference == 'default' && $this->player->tid_steve == 0 && $this->player->tid_alex != 0) {
|
||||
$this->player->setPreference('slim');
|
||||
}
|
||||
|
||||
return json(trans('user.player.set.success', ['name' => $this->player->player_name]), 0);
|
||||
}
|
||||
|
||||
public function clearTexture(Request $request)
|
||||
{
|
||||
$types = array_filter(['steve', 'alex', 'cape'], function ($type) use ($request) {
|
||||
return $request->input($type);
|
||||
});
|
||||
|
||||
$this->player->clearTexture($types);
|
||||
|
||||
return json(trans('user.player.clear.success', ['name' => $this->player->player_name]), 0);
|
||||
}
|
||||
|
||||
public function setPreference(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'preference' => 'required|preference'
|
||||
]);
|
||||
|
||||
$this->player->setPreference($request->preference);
|
||||
|
||||
return json(trans('user.player.preference.success', ['name' => $this->player->player_name, 'preference' => $request->preference]), 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,56 +2,34 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use View;
|
||||
use Datatables;
|
||||
use App\Events;
|
||||
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;
|
||||
use App\Services\PluginManager;
|
||||
|
||||
class PluginController extends Controller
|
||||
{
|
||||
public function config(PluginManager $plugins, $name)
|
||||
public function showManage()
|
||||
{
|
||||
$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'));
|
||||
}
|
||||
return view('admin.plugins');
|
||||
}
|
||||
|
||||
public function config($name, Request $request)
|
||||
{
|
||||
$plugin = plugin($name);
|
||||
|
||||
if ($plugin && $plugin->isEnabled() && $plugin->hasConfigView()) {
|
||||
return $plugin->getConfigView();
|
||||
} 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);
|
||||
$plugin = plugin($name = $request->get('name'));
|
||||
|
||||
if ($plugin) {
|
||||
// Pass the plugin title through the translator.
|
||||
|
|
@ -59,24 +37,37 @@ class PluginController extends Controller
|
|||
|
||||
switch ($request->get('action')) {
|
||||
case 'enable':
|
||||
$result = $plugins->enable($name);
|
||||
if (! $plugins->isRequirementsSatisfied($plugin)) {
|
||||
$reason = [];
|
||||
|
||||
if ($result === true) {
|
||||
return json(trans('admin.plugins.operations.enabled', ['plugin' => $plugin->title]), 0);
|
||||
} else {
|
||||
$reason = $plugins->formatUnresolved($result['unsatisfied'], $result['conflicts']);
|
||||
foreach ($plugins->getUnsatisfiedRequirements($plugin) as $name => $detail) {
|
||||
$constraint = $detail['constraint'];
|
||||
|
||||
return json(trans('admin.plugins.operations.unsatisfied.notice'), 1, compact('reason'));
|
||||
if (! $detail['version']) {
|
||||
$reason[] = trans('admin.plugins.operations.unsatisfied.disabled', compact('name'));
|
||||
} else {
|
||||
$reason[] = trans('admin.plugins.operations.unsatisfied.version', compact('name', 'constraint'));
|
||||
}
|
||||
}
|
||||
|
||||
return json([
|
||||
'errno' => 1,
|
||||
'msg' => trans('admin.plugins.operations.unsatisfied.notice'),
|
||||
'reason' => $reason
|
||||
]);
|
||||
}
|
||||
|
||||
// no break
|
||||
$plugins->enable($name);
|
||||
|
||||
return json(trans('admin.plugins.operations.enabled', ['plugin' => $plugin->title]), 0);
|
||||
|
||||
case 'disable':
|
||||
$plugins->disable($name);
|
||||
|
||||
return json(trans('admin.plugins.operations.disabled', ['plugin' => $plugin->title]), 0);
|
||||
|
||||
case 'delete':
|
||||
$plugins->delete($name);
|
||||
$plugins->uninstall($name);
|
||||
|
||||
return json(trans('admin.plugins.operations.deleted'), 0);
|
||||
|
||||
|
|
@ -90,51 +81,28 @@ class PluginController extends Controller
|
|||
|
||||
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', [])
|
||||
),
|
||||
];
|
||||
$installed = $plugins->getPlugins();
|
||||
|
||||
return Datatables::of($installed)
|
||||
->setRowId('plugin-{{ $name }}')
|
||||
->editColumn('title', '{{ trans($title ?: "EMPTY") }}')
|
||||
->editColumn('description', '{{ trans($description ?: "EMPTY") }}')
|
||||
->addColumn('enabled', function ($plugin) { return $plugin->isEnabled(); })
|
||||
->addColumn('config', function ($plugin) { return $plugin->hasConfigView(); })
|
||||
->addColumn('dependencies', function ($plugin) {
|
||||
return $this->getPluginDependencies($plugin);
|
||||
})
|
||||
->values();
|
||||
->make(true);
|
||||
}
|
||||
|
||||
public function upload(Request $request, PluginManager $manager, Unzip $unzip)
|
||||
protected function getPluginDependencies(Plugin $plugin)
|
||||
{
|
||||
$request->validate(['file' => 'required|file|mimetypes:application/zip']);
|
||||
$plugins = app('plugins');
|
||||
|
||||
$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);
|
||||
}
|
||||
return [
|
||||
'isRequirementsSatisfied' => $plugins->isRequirementsSatisfied($plugin),
|
||||
'requirements' => $plugin->getRequirements(),
|
||||
'unsatisfiedRequirements' => $plugins->getUnsatisfiedRequirements($plugin)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,156 +2,265 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use Log;
|
||||
use File;
|
||||
use Schema;
|
||||
use Option;
|
||||
use Storage;
|
||||
use Artisan;
|
||||
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;
|
||||
use Composer\Semver\Comparator;
|
||||
use Illuminate\Validation\Validator;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
|
||||
class SetupController extends Controller
|
||||
{
|
||||
public function database(
|
||||
Request $request,
|
||||
Filesystem $filesystem,
|
||||
Connection $connection,
|
||||
DatabaseManager $manager,
|
||||
) {
|
||||
if ($request->isMethod('get')) {
|
||||
try {
|
||||
$connection->getPdo();
|
||||
public function welcome()
|
||||
{
|
||||
$type = get_db_type();
|
||||
|
||||
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'),
|
||||
]);
|
||||
if ($type === 'SQLite') {
|
||||
// @codeCoverageIgnoreStart
|
||||
$server = get_db_config()['database'];
|
||||
// @codeCoverageIgnoreEnd
|
||||
} else {
|
||||
$config = get_db_config();
|
||||
$server = "{$config['username']}@{$config['host']}";
|
||||
}
|
||||
|
||||
return view('setup.wizard.welcome')->with(compact('type', 'server'));
|
||||
}
|
||||
|
||||
public function info()
|
||||
{
|
||||
$existingTables = static::checkTablesExist([], true);
|
||||
|
||||
// Not installed completely
|
||||
if (count($existingTables) > 0) {
|
||||
Log::info('[SetupWizard] Remaining tables detected, exit now', [$existingTables]);
|
||||
|
||||
$existingTables = array_map(function ($item) {
|
||||
return get_db_config()['prefix'].$item;
|
||||
}, $existingTables);
|
||||
|
||||
throw new PrettyPageException(trans('setup.database.table-already-exists', ['tables' => json_encode($existingTables)]), 1);
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if (! function_exists('escapeshellarg')) {
|
||||
throw new PrettyPageException(trans('setup.disabled-functions.escapeshellarg'), 1);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return view('setup.wizard.info');
|
||||
}
|
||||
|
||||
public function finish(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:8|max:32|confirmed',
|
||||
'site_name' => 'required'
|
||||
]);
|
||||
|
||||
if ($request->has('generate_random')) {
|
||||
// Generate new APP_KEY & SALT randomly
|
||||
if (is_writable(app()->environmentFile())) {
|
||||
Artisan::call('key:random');
|
||||
Artisan::call('salt:random');
|
||||
|
||||
Log::info('[SetupWizard] Random application key & salt set successfully');
|
||||
} else {
|
||||
// @codeCoverageIgnoreStart
|
||||
Log::warning('[SetupWizard] Failed to set application key since .env is not writable');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
],
|
||||
]);
|
||||
Artisan::call('migrate', ['--force' => true]);
|
||||
Log::info('[SetupWizard] Database migrated');
|
||||
|
||||
Option::set('site_name', $request->get('site_name'));
|
||||
|
||||
$siteUrl = url('/');
|
||||
if (Str::endsWith($siteUrl, '/index.php')) {
|
||||
|
||||
if (ends_with($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();
|
||||
Option::set('site_url', $siteUrl);
|
||||
|
||||
// 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 = User::register(
|
||||
$request->get('email'),
|
||||
$request->get('password'), function ($user)
|
||||
{
|
||||
$user->ip = get_client_ip();
|
||||
$user->score = option('user_initial_score');
|
||||
$user->register_at = get_datetime_string();
|
||||
$user->last_sign_at = get_datetime_string(time() - 86400);
|
||||
$user->permission = User::SUPER_ADMIN;
|
||||
});
|
||||
Log::info('[SetupWizard] Super administrator registered');
|
||||
|
||||
$user->save();
|
||||
$this->createDirectories();
|
||||
Log::info('[SetupWizard] Installation completed');
|
||||
|
||||
$filesystem->put(storage_path('install.lock'), '');
|
||||
return view('setup.wizard.finish')->with([
|
||||
'email' => $request->get('email'),
|
||||
'password' => $request->get('password')
|
||||
]);
|
||||
}
|
||||
|
||||
return view('setup.wizard.finish');
|
||||
public function update()
|
||||
{
|
||||
if (Comparator::lessThanOrEqualTo(config('app.version'), option('version'))) {
|
||||
// No updates available
|
||||
return view('setup.locked');
|
||||
}
|
||||
|
||||
return view('setup.updates.welcome');
|
||||
}
|
||||
|
||||
public function doUpdate()
|
||||
{
|
||||
$resource = opendir(database_path('update_scripts'));
|
||||
$updateScriptExist = false;
|
||||
|
||||
$tips = [];
|
||||
|
||||
while($filename = @readdir($resource)) {
|
||||
if ($filename != "." && $filename != "..") {
|
||||
preg_match('/update-(.*)-to-(.*).php/', $filename, $matches);
|
||||
|
||||
// Skip if the file is not valid or expired
|
||||
if (! isset($matches[2]) ||
|
||||
Comparator::lessThan($matches[2], config('app.version'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = require database_path('update_scripts')."/$filename";
|
||||
|
||||
if (is_array($result)) {
|
||||
// Push the tip into array
|
||||
foreach ($result as $tip) {
|
||||
$tips[] = $tip;
|
||||
}
|
||||
}
|
||||
|
||||
$updateScriptExist = true;
|
||||
}
|
||||
}
|
||||
closedir($resource);
|
||||
|
||||
foreach (config('options') as $key => $value) {
|
||||
if (! Option::has($key)) {
|
||||
Option::set($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $updateScriptExist) {
|
||||
// If there is no update script given
|
||||
Option::set('version', config('app.version'));
|
||||
}
|
||||
|
||||
// Clear all compiled view files
|
||||
try {
|
||||
Artisan::call('view:clear');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[UpdateWizard] Error occured when processing view:clear', [$e]);
|
||||
|
||||
$files = collect(File::files(storage_path('framework/views')));
|
||||
$files->reject(function ($path) {
|
||||
return ends_with($path, '.gitignore');
|
||||
})->each(function ($path) {
|
||||
File::delete($path);
|
||||
});
|
||||
}
|
||||
|
||||
return view('setup.updates.success', ['tips' => $tips]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given tables exist in current database.
|
||||
*
|
||||
* @param array $tables
|
||||
* @param bool $returnExisting
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function checkTablesExist($tables = [], $returnExistingTables = false)
|
||||
{
|
||||
$existingTables = [];
|
||||
$tables = $tables ?: ['users', 'closets', 'players', 'textures', 'options'];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
// Table prefix will be added automatically
|
||||
if (Schema::hasTable($tableName)) {
|
||||
$existingTables[] = $tableName;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($existingTables) == count($tables)) {
|
||||
return true;
|
||||
} else {
|
||||
return $returnExistingTables ? $existingTables : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given columns exist in specific table.
|
||||
* By default, we will check the columns newly added to users table in BS v3.5.0.
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $columns
|
||||
* @return void
|
||||
*/
|
||||
public static function checkNewColumnsExist($table = 'users', $columns = [])
|
||||
{
|
||||
$existingColumns = [];
|
||||
$columns = $columns ?: ['verified', 'verification_token'];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn($table, $column)) {
|
||||
$existingColumns[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
return count($existingColumns) === count($columns);
|
||||
}
|
||||
|
||||
public static function checkDirectories()
|
||||
{
|
||||
$directories = ['storage/textures', 'plugins'];
|
||||
|
||||
try {
|
||||
foreach ($directories as $dir) {
|
||||
if (! Storage::disk('root')->has($dir)) {
|
||||
// Try to mkdir
|
||||
if (! Storage::disk('root')->makeDirectory($dir)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDirectories()
|
||||
{
|
||||
return self::checkDirectories();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function formatValidationErrors(Validator $validator)
|
||||
{
|
||||
return $validator->errors()->all();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,460 +2,389 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Texture;
|
||||
use View;
|
||||
use Option;
|
||||
use Storage;
|
||||
use Session;
|
||||
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 App\Models\Closet;
|
||||
use App\Models\Player;
|
||||
use App\Models\Texture;
|
||||
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;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
|
||||
class SkinlibController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
protected $user = null;
|
||||
|
||||
/**
|
||||
* Map error code of file uploading to human-readable text.
|
||||
*
|
||||
* @see http://php.net/manual/en/features.file-upload.errors.php
|
||||
* @var array
|
||||
*/
|
||||
public static $phpFileUploadErrors = [
|
||||
0 => 'There is no error, the file uploaded with success',
|
||||
1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
|
||||
2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
|
||||
3 => 'The uploaded file was only partially uploaded',
|
||||
4 => 'No file was uploaded',
|
||||
6 => 'Missing a temporary folder',
|
||||
7 => 'Failed to write file to disk.',
|
||||
8 => 'A PHP extension stopped the file upload.',
|
||||
];
|
||||
|
||||
public function __construct(UserRepository $users)
|
||||
{
|
||||
$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']);
|
||||
// Try to load user by uid stored in session.
|
||||
// If there is no uid stored in session or the uid is invalid
|
||||
// it will return a null value.
|
||||
$this->user = $users->get(session('uid'));
|
||||
}
|
||||
|
||||
public function library(Request $request)
|
||||
public function index()
|
||||
{
|
||||
return view('skinlib.index', ['user' => $this->user]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skin library data filtered.
|
||||
* Available Query String: filter, uploader, page, sort, keyword, items_per_page.
|
||||
*
|
||||
* @param Request $request [description]
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function getSkinlibFiltered(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Available filters: skin, steve, alex, cape
|
||||
$type = $request->input('filter', 'skin');
|
||||
$uploader = $request->input('uploader');
|
||||
$keyword = $request->input('keyword');
|
||||
$filter = $request->input('filter', 'skin');
|
||||
|
||||
// Filter result by uploader's uid
|
||||
$uploader = intval($request->input('uploader', 0));
|
||||
|
||||
// Available sorting methods: time, likes
|
||||
$sort = $request->input('sort', 'time');
|
||||
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
|
||||
$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);
|
||||
// Current page
|
||||
$page = $request->input('page', 1);
|
||||
$currentPage = ($page <= 0) ? 1 : $page;
|
||||
|
||||
// How many items to show in one page
|
||||
$itemsPerPage = $request->input('items_per_page', 20);
|
||||
$itemsPerPage = $itemsPerPage <= 0 ? 20 : $itemsPerPage;
|
||||
|
||||
// Keyword to search
|
||||
$keyword = $request->input('keyword', '');
|
||||
|
||||
// Check if user logged in
|
||||
$anonymous = is_null($this->user);
|
||||
|
||||
if ($filter == "skin") {
|
||||
$query = Texture::where(function ($innerQuery) {
|
||||
// Nested condition, DO NOT MODIFY
|
||||
$innerQuery->where('type', '=', 'steve')->orWhere('type', '=', 'alex');
|
||||
});
|
||||
} else {
|
||||
$query = Texture::where('type', $filter);
|
||||
}
|
||||
|
||||
if ($keyword !== "") {
|
||||
$query = $query->like('name', $keyword);
|
||||
}
|
||||
|
||||
if ($uploader !== 0) {
|
||||
$query = $query->where('uploader', $uploader);
|
||||
}
|
||||
|
||||
if ($anonymous) {
|
||||
// Show public textures only to anonymous visitors
|
||||
$query = $query->where('public', 1);
|
||||
} else {
|
||||
// Show private textures when show uploaded textures of current user
|
||||
if ($uploader != $this->user->uid && !$this->user->isAdmin()) {
|
||||
$query = $query->where(function ($innerQuery) {
|
||||
$innerQuery->where('public', '=', '1')->orWhere('uploader', '=', $this->user->uid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$totalPages = ceil($query->count() / $itemsPerPage);
|
||||
|
||||
$textures = $query->orderBy($sortBy, 'desc')
|
||||
->skip(($currentPage - 1) * $itemsPerPage)
|
||||
->take($itemsPerPage)
|
||||
->get();
|
||||
|
||||
if (! $anonymous) {
|
||||
foreach ($textures as $item) {
|
||||
$item->liked = $this->user->getCloset()->has($item->tid);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'items' => $textures,
|
||||
'anonymous' => $anonymous,
|
||||
'total_pages' => $totalPages
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Filter $filter, Texture $texture)
|
||||
public function show($tid)
|
||||
{
|
||||
/** @var User */
|
||||
$user = Auth::user();
|
||||
/** @var FilesystemAdapter */
|
||||
$disk = Storage::disk('textures');
|
||||
$texture = Texture::find($tid);
|
||||
|
||||
if ($disk->missing($texture->hash)) {
|
||||
if (! $texture || $texture && !Storage::disk('textures')->has($texture->hash)) {
|
||||
if (option('auto_del_invalid_texture')) {
|
||||
$texture->delete();
|
||||
if ($texture) {
|
||||
$texture->delete();
|
||||
}
|
||||
|
||||
abort(404, trans('skinlib.show.deleted'));
|
||||
}
|
||||
abort(404, trans('skinlib.show.deleted'));
|
||||
abort(404, trans('skinlib.show.deleted').trans('skinlib.show.contact-admin'));
|
||||
}
|
||||
|
||||
$badges = [];
|
||||
$uploader = $texture->owner;
|
||||
if ($uploader) {
|
||||
if ($uploader->isAdmin()) {
|
||||
$badges[] = ['text' => 'STAFF', 'color' => 'primary'];
|
||||
}
|
||||
|
||||
$badges = $filter->apply('user_badges', $badges, [$uploader]);
|
||||
if ($texture->public == "0") {
|
||||
if (is_null($this->user) || ($this->user->uid != $texture->uploader && !$this->user->isAdmin()))
|
||||
abort(403, trans('skinlib.show.private'));
|
||||
}
|
||||
|
||||
$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,
|
||||
]);
|
||||
return view('skinlib.show')->with('texture', $texture)->with('with_out_filter', true)->with('user', $this->user);
|
||||
}
|
||||
|
||||
public function info(Texture $texture)
|
||||
public function info($tid)
|
||||
{
|
||||
return $texture;
|
||||
if ($t = Texture::find($tid)) {
|
||||
return json($t->toArray());
|
||||
} else {
|
||||
return json([]);
|
||||
}
|
||||
}
|
||||
|
||||
public function upload(Filter $filter)
|
||||
public function upload()
|
||||
{
|
||||
$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(),
|
||||
]);
|
||||
return view('skinlib.upload')->with('user', $this->user)->with('with_out_filter', true);
|
||||
}
|
||||
|
||||
public function handleUpload(
|
||||
Request $request,
|
||||
Filter $filter,
|
||||
Dispatcher $dispatcher,
|
||||
) {
|
||||
$file = $request->file('file');
|
||||
if ($file && !$file->isValid()) {
|
||||
Log::error($file->getErrorMessage());
|
||||
public function handleUpload(Request $request)
|
||||
{
|
||||
if (($response = $this->checkUpload($request)) instanceof JsonResponse) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$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',
|
||||
]);
|
||||
$t = new Texture();
|
||||
$t->name = $request->input('name');
|
||||
$t->type = $request->input('type');
|
||||
$t->likes = 1;
|
||||
$t->hash = bs_hash_file($request->file('file'));
|
||||
$t->size = ceil($request->file('file')->getSize() / 1024);
|
||||
$t->public = ($request->input('public') == 'true') ? "1" : "0";
|
||||
$t->uploader = $this->user->uid;
|
||||
$t->upload_at = get_datetime_string();
|
||||
|
||||
/** @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 = $t->size * (($t->public == "1") ? Option::get('score_per_storage') : Option::get('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);
|
||||
|
||||
if ($this->user->getScore() < $cost)
|
||||
return json(trans('skinlib.upload.lack-score'), 7);
|
||||
|
||||
$results = Texture::where('hash', $t->hash)->get();
|
||||
|
||||
if (! $results->isEmpty()) {
|
||||
foreach ($results as $result) {
|
||||
// if the texture already uploaded was set to private,
|
||||
// then allow to re-upload it.
|
||||
if ($result->type == $t->type && $result->public == "1") {
|
||||
return json(trans('skinlib.upload.repeated'), 0, [
|
||||
'tid' => $result->tid
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
if (! Storage::disk('textures')->exists($t->hash)) {
|
||||
Storage::disk('textures')->put($t->hash, file_get_contents($request->file('file')));
|
||||
}
|
||||
|
||||
$user->score -= $cost;
|
||||
$user->closet()->attach($texture->tid, ['item_name' => $name]);
|
||||
$user->save();
|
||||
$t->save();
|
||||
|
||||
$dispatcher->dispatch('texture.uploaded', [$texture, $image]);
|
||||
$this->user->setScore($cost, 'minus');
|
||||
|
||||
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
|
||||
'tid' => $texture->tid,
|
||||
]);
|
||||
}
|
||||
if ($this->user->getCloset()->add($t->tid, $t->name)) {
|
||||
return json(trans('skinlib.upload.success', ['name' => $request->input('name')]), 0, [
|
||||
'tid' => $t->tid
|
||||
]);
|
||||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter)
|
||||
public function delete(Request $request, UserRepository $users)
|
||||
{
|
||||
$can = $filter->apply('can_delete_texture', true, [$texture]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
$result = Texture::find($request->tid);
|
||||
|
||||
if (! $result) {
|
||||
return json(trans('skinlib.non-existent'), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('texture.deleting', [$texture]);
|
||||
if ($result->uploader != $this->user->uid && !$this->user->isAdmin()) {
|
||||
return json(trans('skinlib.no-permission'), 1);
|
||||
}
|
||||
|
||||
// check if file occupied
|
||||
if (Texture::where('hash', $texture->hash)->count() === 1) {
|
||||
Storage::disk('textures')->delete($texture->hash);
|
||||
if (Texture::where('hash', $result->hash)->count() == 1) {
|
||||
Storage::disk('textures')->delete($result->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]);
|
||||
if (option('return_score')) {
|
||||
if ($u = $users->get($result->uploader)) {
|
||||
if ($result->public == 1) {
|
||||
$u->setScore(
|
||||
$result->size * option('score_per_storage'), 'plus'
|
||||
);
|
||||
} else {
|
||||
$u->setScore(
|
||||
$result->size * option('private_score_per_storage'), 'plus'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('texture.privacy.updating', [$texture]);
|
||||
if ($result->delete()) {
|
||||
return json(trans('skinlib.delete.success'), 0);
|
||||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
$uploader->score += $score_diff;
|
||||
$uploader->save();
|
||||
public function privacy(Request $request, UserRepository $users)
|
||||
{
|
||||
$t = Texture::find($request->input('tid'));
|
||||
|
||||
$texture->public = !$texture->public;
|
||||
$texture->save();
|
||||
if (! $t)
|
||||
return json(trans('skinlib.non-existent'), 1);
|
||||
|
||||
$dispatcher->dispatch('texture.privacy.updated', [$texture]);
|
||||
if ($t->uploader != $this->user->uid && !$this->user->isAdmin())
|
||||
return json(trans('skinlib.no-permission'), 1);
|
||||
|
||||
$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);
|
||||
$score_diff = $t->size * (option('private_score_per_storage') - option('score_per_storage')) * ($t->public == 1 ? -1 : 1);
|
||||
if ($users->get($t->uploader)->getScore() + $score_diff < 0) {
|
||||
return json(trans('skinlib.upload.lack-score'), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('texture.name.updating', [$texture, $name]);
|
||||
$type = $t->type;
|
||||
Player::where("tid_$type", $t->tid)
|
||||
->where('uid', '<>', session('uid'))
|
||||
->get()
|
||||
->each(function ($player) use ($type) {
|
||||
$player->setTexture(["tid_$type" => 0]);
|
||||
});
|
||||
|
||||
$old = $texture->replicate();
|
||||
$texture->name = $name;
|
||||
$texture->save();
|
||||
@$users->get($t->uploader)->setScore($score_diff, 'plus');
|
||||
|
||||
$dispatcher->dispatch('texture.name.updated', [$texture, $old]);
|
||||
if ($t->setPrivacy(!$t->public)) {
|
||||
return json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('skinlib.privacy.success', ['privacy' => ($t->public == "0" ? trans('general.private') : trans('general.public'))]),
|
||||
'public' => $t->public
|
||||
]);
|
||||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
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'])],
|
||||
public function rename(Request $request) {
|
||||
$this->validate($request, [
|
||||
'tid' => 'required|integer',
|
||||
'new_name' => 'required|no_special_chars'
|
||||
]);
|
||||
$type = $data['type'];
|
||||
|
||||
$can = $filter->apply('can_update_texture_type', true, [$texture, $type]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
$t = Texture::find($request->input('tid'));
|
||||
|
||||
if (! $t)
|
||||
return json(trans('skinlib.non-existent'), 1);
|
||||
|
||||
if ($t->uploader != $this->user->uid && !$this->user->isAdmin())
|
||||
return json(trans('skinlib.no-permission'), 1);
|
||||
|
||||
$t->name = $request->input('new_name');
|
||||
|
||||
if ($t->save()) {
|
||||
return json(trans('skinlib.rename.success', ['name' => $request->input('new_name')]), 0);
|
||||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
public function model(Request $request) {
|
||||
$this->validate($request, [
|
||||
'tid' => 'required|integer',
|
||||
'model' => 'required|in:steve,alex,cape'
|
||||
]);
|
||||
|
||||
$t = Texture::find($request->input('tid'));
|
||||
|
||||
if (! $t)
|
||||
return json(trans('skinlib.non-existent'), 1);
|
||||
|
||||
if ($t->uploader != $this->user->uid && !$this->user->isAdmin())
|
||||
return json(trans('skinlib.no-permission'), 1);
|
||||
|
||||
$duplicate = Texture::where('hash', $t->hash)
|
||||
->where('type', $request->input('model'))
|
||||
->where('tid', '<>', $t->tid)
|
||||
->first();
|
||||
|
||||
if ($duplicate && $duplicate->public) {
|
||||
return json(trans('skinlib.model.duplicate', ['tid' => $duplicate->tid]), 1);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('texture.type.updating', [$texture, $type]);
|
||||
$t->type = $request->input('model');
|
||||
$t->save();
|
||||
|
||||
$old = $texture->replicate();
|
||||
$texture->type = $type;
|
||||
$texture->save();
|
||||
|
||||
$dispatcher->dispatch('texture.type.updated', [$texture, $old]);
|
||||
|
||||
return json(trans('skinlib.model.success', ['model' => $type]), 0);
|
||||
return json(trans('skinlib.model.success', ['model' => request('model')]), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Uploaded Files
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function checkUpload(Request $request)
|
||||
{
|
||||
if ($file = $request->files->get('file')) {
|
||||
if ($file->getError() !== UPLOAD_ERR_OK) {
|
||||
return json(static::$phpFileUploadErrors[$file->getError()], $file->getError());
|
||||
}
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required|'.(option('texture_name_regexp') ? 'texture_name_regexp' : 'no_special_chars'),
|
||||
'file' => 'required|max:'.option('max_upload_file_size'),
|
||||
'public' => 'required'
|
||||
]);
|
||||
|
||||
if (extension_loaded('fileinfo')) {
|
||||
$mime = $request->file('file')->getMimeType();
|
||||
} else {
|
||||
$mime = $_FILES['file']['type'];
|
||||
}
|
||||
|
||||
if ($mime != "image/png" && $mime != "image/x-png") {
|
||||
return json(trans('skinlib.upload.type-error'), 1);
|
||||
}
|
||||
|
||||
$type = $request->input('type');
|
||||
$size = getimagesize($request->file('file'));
|
||||
$ratio = $size[0] / $size[1];
|
||||
|
||||
if ($type == "steve" || $type == "alex") {
|
||||
if ($ratio != 2 && $ratio != 1)
|
||||
return json(trans('skinlib.upload.invalid-size', ['type' => trans('general.skin'), 'width' => $size[0], 'height' => $size[1]]), 1);
|
||||
if ($size[0] % 64 != 0 || $size[1] % 32 != 0)
|
||||
return json(trans('skinlib.upload.invalid-hd-skin', ['type' => trans('general.skin'), 'width' => $size[0], 'height' => $size[1]]), 1);
|
||||
} elseif ($type == "cape") {
|
||||
if ($ratio != 2)
|
||||
return json(trans('skinlib.upload.invalid-size', ['type' => trans('general.cape'), 'width' => $size[0], 'height' => $size[1]]), 1);
|
||||
} else {
|
||||
return json(trans('general.illegal-parameters'), 1);
|
||||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,179 +2,261 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Event;
|
||||
use Option;
|
||||
use Storage;
|
||||
use Response;
|
||||
use Minecraft;
|
||||
use Exception;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\User;
|
||||
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;
|
||||
use App\Events\GetSkinPreview;
|
||||
use App\Events\GetAvatarPreview;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
class TextureController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
/**
|
||||
* Return Player Profile formatted in JSON.
|
||||
*
|
||||
* @param string $player_name
|
||||
* @param string $api
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function json($player_name, $api = "")
|
||||
{
|
||||
$this->middleware('cache.headers:public;max_age='.option('cache_expire_time'))
|
||||
->only(['json']);
|
||||
$player = $this->getPlayerInstance($player_name);
|
||||
|
||||
$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);
|
||||
if ($api == "csl") {
|
||||
$content = $player->getJsonProfile(Player::CSL_API);
|
||||
} else if ($api == "usm") {
|
||||
$content = $player->getJsonProfile(Player::USM_API);
|
||||
} else {
|
||||
$content = $player->getJsonProfile(Option::get('api_type'));
|
||||
}
|
||||
|
||||
$size = (int) $request->query('size', 100);
|
||||
$mode = $request->has('3d') ? '3d' : '2d';
|
||||
$usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
|
||||
$format = $usePNG ? 'png' : 'webp';
|
||||
return Response::jsonProfile($content, 200, [
|
||||
'Last-Modified' => strtotime($player->last_modified)
|
||||
]);
|
||||
}
|
||||
|
||||
$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);
|
||||
public function jsonWithApi($api, $player_name)
|
||||
{
|
||||
return $this->json($player_name, $api);
|
||||
}
|
||||
|
||||
public function texture($hash, $headers = [], $message = '') {
|
||||
try {
|
||||
if (Storage::disk('textures')->has($hash)) {
|
||||
return Response::png(Storage::disk('textures')->get($hash), 200, array_merge([
|
||||
'Last-Modified' => Storage::disk('textures')->lastModified($hash),
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Content-Length' => Storage::disk('textures')->size($hash),
|
||||
], $headers));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Let it fallback to 404
|
||||
report($e);
|
||||
}
|
||||
|
||||
$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);
|
||||
return abort(404, $message);
|
||||
}
|
||||
|
||||
public function textureWithApi($api, $hash) {
|
||||
return $this->texture($hash);
|
||||
}
|
||||
|
||||
public function skin($player_name, $model = "")
|
||||
{
|
||||
$player = $this->getPlayerInstance($player_name);
|
||||
|
||||
$model_preference = ($player->getPreference() == "default") ? "steve" : "alex";
|
||||
$model = ($model == "") ? $model_preference : $model;
|
||||
|
||||
return $this->getBinaryTextureFromPlayer($player_name, $model);
|
||||
}
|
||||
|
||||
public function skinWithModel($model, $player_name)
|
||||
{
|
||||
return $this->skin($player_name, $model);
|
||||
}
|
||||
|
||||
public function cape($player_name)
|
||||
{
|
||||
return $this->getBinaryTextureFromPlayer($player_name, 'cape');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the texture image of given type and player.
|
||||
*
|
||||
* @param string $player_name
|
||||
* @param string $type "steve" or "alex" or "cape".
|
||||
* @return void|Response
|
||||
*/
|
||||
protected function getBinaryTextureFromPlayer($player_name, $type)
|
||||
{
|
||||
$player = $this->getPlayerInstance($player_name);
|
||||
|
||||
if ($hash = $player->getTexture($type)) {
|
||||
return $this->texture($hash, [
|
||||
'Last-Modified' => strtotime($player->last_modified)
|
||||
], trans('general.texture-deleted'));
|
||||
} else {
|
||||
abort(404, trans('general.texture-not-uploaded', ['type' => $type]));
|
||||
}
|
||||
}
|
||||
|
||||
public function avatar($base64_email, UserRepository $users, $size = 128)
|
||||
{
|
||||
$user = $users->get(base64_decode($base64_email), 'email');
|
||||
|
||||
if ($user) {
|
||||
$tid = $user->getAvatarId();
|
||||
|
||||
if ($t = Texture::find($tid)) {
|
||||
try {
|
||||
if (Storage::disk('textures')->has($t->hash)) {
|
||||
$responses = event(new GetAvatarPreview($t, $size));
|
||||
|
||||
if (isset($responses[0]) && $responses[0] instanceof SymfonyResponse) {
|
||||
return $responses[0]; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$png = Minecraft::generateAvatarFromSkin(Storage::disk('textures')->read($t->hash), $size);
|
||||
|
||||
ob_start();
|
||||
imagepng($png);
|
||||
imagedestroy($png);
|
||||
$image = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return Response::png($image);
|
||||
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Let it fallback to default avatar
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$png = imagecreatefromstring(base64_decode(static::getDefaultAvatar()));
|
||||
ob_start();
|
||||
imagepng($png);
|
||||
imagedestroy($png);
|
||||
$image = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return Response::png($image);
|
||||
}
|
||||
|
||||
public function avatarWithSize($size, $base64_email, UserRepository $users)
|
||||
{
|
||||
return $this->avatar($base64_email, $users, $size);
|
||||
}
|
||||
|
||||
public function preview($tid, $size = 250)
|
||||
{
|
||||
if ($t = Texture::find($tid)) {
|
||||
try {
|
||||
if (Storage::disk('textures')->has($t->hash)) {
|
||||
$responses = event(new GetSkinPreview($t, $size));
|
||||
|
||||
if (isset($responses[0]) && $responses[0] instanceof SymfonyResponse) {
|
||||
return $responses[0]; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$binary = Storage::disk('textures')->read($t->hash);
|
||||
|
||||
if ($t->type == "cape") {
|
||||
$png = Minecraft::generatePreviewFromCape($binary, $size*0.8, $size*1.125, $size);
|
||||
} else {
|
||||
$png = Minecraft::generatePreviewFromSkin($binary, $size, ($t->type == 'alex'), 'both', 4);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
imagepng($png);
|
||||
imagedestroy($png);
|
||||
$image = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return Response::png($image);
|
||||
}
|
||||
|
||||
$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);
|
||||
} catch (Exception $e) {
|
||||
// Let it fallback to default preview
|
||||
report($e);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
// Show this if given texture is invalid.
|
||||
$png = imagecreatefromstring(base64_decode(static::getBrokenPreview()));
|
||||
ob_start();
|
||||
imagepng($png);
|
||||
imagedestroy($png);
|
||||
$image = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return Response::png($image);
|
||||
}
|
||||
|
||||
public function previewWithSize($size, $tid)
|
||||
{
|
||||
return $this->preview($tid, $size);
|
||||
}
|
||||
|
||||
public function raw($tid) {
|
||||
if ($t = Texture::find($tid)) {
|
||||
return $this->texture($t->hash);
|
||||
} else {
|
||||
return abort(404, trans('skinlib.non-existent'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getPlayerInstance($player_name)
|
||||
{
|
||||
$player = Player::where('player_name', $player_name)->first();
|
||||
|
||||
if ($player->isBanned()) {
|
||||
abort(403, trans('general.player-banned'));
|
||||
}
|
||||
|
||||
return $player;
|
||||
}
|
||||
|
||||
public static function getDefaultAvatar()
|
||||
{
|
||||
return "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAACSElEQVR4nO3csUpbcQBGca/epBDTFio0KCLiC/QNRJBCod10LkUwfQq7uYhDKa5Ondri4NLRoX2GLl1EVGqhQcVEiYnSZ/jAy8lwfvN3r5LDf7lXU7yYfzqWGNzdR/vaxHil9x8Oh9E+VZZlpffPPh09OAPADAAzAMwAMAPADAAzAMwAMAPADAAzAMwAMAPADAAr0+fvqarfH0w1G9E+1eneVnp/TwDMADADwAwAMwDMADADwAwAMwDMADADwAwAMwDMADADwOI/ft9afxftG416+iNGyr/Lq2i/+flLtPcEwAwAMwDMADADwAwAMwDMADADwAwAMwDMADADwAwAMwCs+LrRji5o1B5F++tBv9L7n12cR/uFmVa0P/z9K9qnPAEwA8AMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMACvT5++vNz5F+5XFrWi/9vIk2qfP93f2n0f7vR/b0f7bh/fR3hMAMwDMADADwAwAMwDMADADwAwAMwDMADADwAwAMwDMALBit/0quuBxay7ap+8bvh/8jPap1TfL0T79/4A/nU609wTADAAzAMwAMAPADAAzAMwAMAPADAAzAMwAMAPADAAzAKz4+HYpuqBeFNH+6Lgb7UfN9OxktO/2etHeEwAzAMwAMAPADAAzAMwAMAPADAAzAMwAMAPADAAzAMwAsPL0b/b9+71+9j5gZqoW7VOX3dto/+xJM9rf3Ayiffr7eAJgBoAZAGYAmAFgBoAZAGYAmAFgBoAZAGYAmAFgBoAZAPYfTCpLwD1OEBAAAAAASUVORK5CYII=";
|
||||
}
|
||||
|
||||
public static function getBrokenPreview()
|
||||
{
|
||||
return "iVBORw0KGgoAAAANSUhEUgAAAJYAAACFCAMAAACOnfHlAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAMAUExURQAAAFNTUwICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTExQUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJWVlZaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///xryqC4AAAABdFJOUwBA5thmAAAAeElEQVR42uzVMQ6AIAxAUbz/pZkYjECMktTiext0+QMppQAAAAAAAAAsdozJSpQ1uoksk7UoK7JM1qOs7pOSlTcrftfLercg7k9lyZIlS5asXbOu/7OsXFmngnaQlSvL3pIlKzZrTtb3swAAAAAAAP6oAgAA//8DANVuAg69lXAOAAAAAElFTkSuQmCC";
|
||||
}
|
||||
|
||||
/**
|
||||
* Default steve skin, base64 encoded.
|
||||
*
|
||||
* @see https://minecraft.gamepedia.com/File:Steve_skin.png
|
||||
* @return string
|
||||
*/
|
||||
public static function getDefaultSteveSkin()
|
||||
{
|
||||
return 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFDUlEQVR42u2a20sUURzH97G0LKMotPuWbVpslj1olJXdjCgyisowsSjzgrB0gSKyC5UF1ZNQWEEQSBQ9dHsIe+zJ/+nXfM/sb/rN4ZwZ96LOrnPgyxzP/M7Z+X7OZc96JpEISfWrFhK0YcU8knlozeJKunE4HahEqSc2nF6zSEkCgGCyb+82enyqybtCZQWAzdfVVFgBJJNJn1BWFgC49/VpwGVlD0CaxQiA5HSYEwBM5sMAdKTqygcAG9+8coHKY/XXAZhUNgDYuBSPjJL/GkzVVhAEU5tqK5XZ7cnFtHWtq/TahdSw2l0HUisr1UKIWJQBAMehDuqiDdzndsP2EZECAG1ZXaWMwOCODdXqysLf++uXUGv9MhUHIByDOijjdiSAoH3ErANQD73C7TXXuGOsFj1d4YH4OTJAEy8y9Hd0mCaeZ5z8dfp88zw1bVyiYhCLOg1ZeAqC0ybaDttHRGME1DhDeVWV26u17lRAPr2+mj7dvULfHw2q65fhQRrLXKDfIxkau3ZMCTGIRR3URR5toU38HbaPiMwUcKfBAkoun09PzrbQ2KWD1JJaqswjdeweoR93rirzyCMBCmIQizqoizZkm2H7iOgAcHrMHbbV9KijkUYv7qOn55sdc4fo250e+vUg4329/Xk6QB/6DtOws+dHDGJRB3XRBve+XARt+4hIrAF4UAzbnrY0ve07QW8uHfB+0LzqanMM7qVb+3f69LJrD90/1axiEIs6qIs21BTIToewfcSsA+Bfb2x67OoR1aPPzu2i60fSNHRwCw221Suz0O3jO+jh6V1KyCMGse9721XdN5ePutdsewxS30cwuMjtC860T5JUKpXyKbSByUn7psi5l+juDlZYGh9324GcPKbkycaN3jUSAGxb46IAYPNZzW0AzgiQ5tVnzLUpUDCAbakMQXXrOtX1UMtHn+Q9/X5L4wgl7t37r85OSrx+TYl379SCia9KXjxRpiTjIZTBFOvrV1f8ty2eY/T7XJ81FQAwmA8ASH1ob68r5PnBsxA88/xAMh6SpqW4HRnLBrkOA9Xv5wPAZjAUgOkB+SHxgBgR0qSMh0zmZRsmwDJm1gFg2PMDIC8/nAHIMls8x8GgzOsG5WiaqREgYzDvpTwjLDy8NM15LpexDEA3LepjU8Z64my+8PtDCmUyRr+fFwA2J0eAFYA0AxgSgMmYBMZTwFQnO9RNAEaHOj2DXF5UADmvAToA2ftyxZYA5BqgmZZApDkdAK4mAKo8GzPlr8G8AehzMAyA/i1girUA0HtYB2CaIkUBEHQ/cBHSvwF0AKZFS5M0ZwMQtEaEAmhtbSUoDADH9ff3++QZ4o0I957e+zYAMt6wHkhzpjkuAcgpwNcpA7AZDLsvpwiuOkBvxygA6Bsvb0HlaeKIF2EbADZpGiGzBsA0gnwQHGOhW2snRpbpPexbAB2Z1oicAMQpTnGKU5ziFKc4xSlOcYpTnOIUpzgVmgo+XC324WfJAdDO/+ceADkCpuMFiFKbApEHkOv7BfzfXt+5gpT8V7rpfYJcDz+jAsB233r6yyBsJ0mlBCDofuBJkel4vOwBFPv8fyYAFPJ+wbSf/88UANNRVy4Awo6+Ig2gkCmgA5DHWjoA+X7AlM//owLANkX0w0359od++pvX8fdMAcj3/QJ9iJsAFPQCxHSnQt8vMJ3v2wCYpkhkAOR7vG7q4aCXoMoSgG8hFAuc/grMdAD4B/kHl9da7Ne9AAAAAElFTkSuQmCC';
|
||||
}
|
||||
|
||||
/**
|
||||
* Default alex skin, base64 encoded.
|
||||
*
|
||||
* @see https://minecraft.gamepedia.com/File:Alex_skin.png
|
||||
* @return string
|
||||
*/
|
||||
public static function getDefaultAlexSkin() {
|
||||
return 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAIAElEQVR42uWa728URRjH968wanznj5DoC9BASIopJFKgHFIbCUmlNQVLGhoDoqRa0rQaAgJFDRoSEuGNJmITrVrPKIgmGo0EXlg1/CYkBIwoUOUtL9b7Pt3v9NnnZq/X2722XDf5ZmZndu/2+5lnZnd2Nggm2K4dXBaK9tWHRflCerb/8ZIKUm6XD+bGNLC0OF9IR/oWllTqTZslgIvvLpL0r0M5Z3Rwd1tMmQFQZgng9P4lkl451OyMfrS7PabsAESGYZYgBEAEhObf394UEyFkEQEwDLMEIQAiIDR/sKc5JkLIJAJiAFQ3QHnVAUSGHQDVDVA+JQAgAaC6A/YB4PLPH1QdACQAVHfAPgBc+unDKgMwg57u/1DVAZhBT/d/qOoAXH/XdwEVBQTgUxYAXH/XdwEVBQTgUyYA9ODHVrddoJTS/r8e/NjqtguUUtkhTqM0afu8Sz23Rd9xGpwv5e9rczq0bZ93qee26DtOg/Ol/P0g0UjChXMMsGaSoLlyTxShLNFIwoVzDLBmkqC5ck8UoSwodeHWKMz/8/uwKNbSBGZa3oLRUcbfLHXh1ijMX/8tL4q1NIGZlrdgdJQ5ALZV2bdZxjzN3z5/NLx68mORHhTdb+hWNpB83cK2qr0D6A3G/z33bcljYq1sIPm6RRALadPKyMPonTt3wgfXPyTmb1/6QfIoQ51+Kiw1JmjAOh8L6cgMWxnby40PBIX/CuZvX+BMIo8y1OnHZd9t044XOrKQd7c5try0csGkmC3o1umvwtFzx5x5SS8cD2+d+UbyBOU16ukWsf2CeJvjqC6tfPFHaWnb2iy3UeEgWqOebhHbly5g+jzDXMzScJSOHvtcRDiE5SAkjPR6ULVji+3zDHMxSxBRevP4sIhwCMtBSBjp9aBqx5bYkx5DX0eAky5TcIrMT3Rb5HjBByl1wQx9HQFOukzBKTI/0W2RY05UVrS9sOSxUGvOnDkxTfRc0ZPvDnfuai96L0AAnUvnltSEvz/UHVJnj74nDYD0mXkPB1DqLSsAWgDA8M8EQP4uAcDHYeSrAYDmkW5aPDeAZgwAN5gWpMeGLLtAJgAc0YgqLuK1NU+G/S2Lwxcbn5B9gGA68N0eUe8XPSJ3bnQ+6iwA+zTonXeY2+jr+b5w79E3Q93i/H2Uy3+ba0c564OhoUBUTovzx2Fy69MLBACkzTO/OTdf4EA4Vl8AL3DP3o0xAIwCfSfg8XbuAeM07wBQ+W5nHv9j/5vnoG5SALRRGiSMpHoAEgCeFpIL3Nc5Pm9QzwDWEA1TbMEYABVlupVtBLB80gAo3RdpcLIAuoe2uW4CCBDNx7pMdJ6+aG0cxyLtf6Ml3LV7vQh5dK+33+mStFRdRQAY4jvbGkTI23o7aNkW0gAYqmL+yx5vSPsAOIAFXT1xJCZGFVSqbrB3YwBNuNXV1YUUDb767CIRW14f09DQEFMsAgopTWtT2lwMwFC8T2sATK05/RiuQbCOadkAVqxYEVIw2NLSEra2toqQR5k+ZtWqVeHatWtFyFsAtvWtIdtldIszz3MJQJslgOu/Dsn+jT++LsxKByXVj+oVAYDa29vDrq4uEfK2vghAPt4FeOHsCtpMDIA5Xh+rz4ExGkV6fWQcwui541J+7eQn4ej578fhjAxPHgBb2gdAR4EFcO/Ce8L76+9z6er+XAyAHhNQh2N8x8uxn70SOwd1MKglAKLJGPZhnHVukjaZCIAJqLm52XWBDRs2iNgFUMfjGhsbw6amJhHyOvyRwhilzXJfD4CQrddwIJqDUWllmrz8SzGAQhnq/i7kD2x+LoAmBWDdunVFEYCyyQCwfZ63s9gY4Dnejhssp3GaFZORUdaxnuVQ2QDq6+tDCmbR8lu2bBEhjzJ9zPLly8NcLidC3vc4irvHtqaFMaFM7gL5ZABWqBPjUV93AFQX0FFQEYDp2jo6Oio7cWAgCAYHJcXkh9PgzKbDM36D+VOnJOUMcHYCKEQATRNEJu8DZvymuoA1X5sA2tqCYNOmce3YMQYB0uXQ/v1j6ukZ112/rVwZBJ2dY+rrG5vhHTkSBIcPj+1Dug4iCKgmIkADgHkanZUA0PIwOVsiYOtTjwYHnl8i2rWmLuAzPoR9iPUsf6tzjVNNAsATnqSqvGYBdNQ94gUgmm0AtEkLQHePmgRgW1kDsONDTY4BFoAeA2x09LbmnGoaQNJdoCYBUDCF0Eb60rJ5Qe/qBU4sn1YAaRdXZfXIs/xd9mSHk6Voujzl7wvSAtBLaBUBgPkIxLS8L8gCANYS7fJ32QYIoJBOy/uC1ABg3vMBRNkApvp9gf1+AN8UcPFUL6vr5XW98KFfkLL1Sy2j4Rwc4yY+0/2+wK4ec2XZri4nfl+gDPL7gaKl8VLr/3hfwI0zQkYCZ4u6LuvZogUAU0hhkKvLZS2vm/V/3xcg3vV/RIAGgPcF2PR0udoA9NI5vzDxhb8PQOr1fwsAxrHpFyZTBaCS7wvSrv/jQYkb5wOMAD4pYqvabDHt9wVp1/+TAOi5QlUBpP2+IGn9n2b18rhv/R+zxRkDoJLvC7RBrAHq9X+WY/0/CcCVM58GN/88IUL+6oXh4L8bI5JyX+dZlzmASr8vmGj9P6nOFwFsZcmr9wU6OjKPgLTfFxSt8EYGYTapzkZAUiuXqststpj2+wK99u9MqvV/DcHVFeQbBHUE6PcFdgzIdLqc9vsCGGNft2v83k9gPAB87wtQV877gv8BjY2wPg7jcKEAAAAASUVORK5CYII=';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,92 +2,292 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Unzip;
|
||||
use Log;
|
||||
use File;
|
||||
use Cache;
|
||||
use Composer\CaBundle\CaBundle;
|
||||
use Option;
|
||||
use Storage;
|
||||
use Exception;
|
||||
use ZipArchive;
|
||||
use App\Services\OptionForm;
|
||||
use Illuminate\Http\Request;
|
||||
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;
|
||||
/**
|
||||
* Current application version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $currentVersion;
|
||||
|
||||
/**
|
||||
* Latest application version in update source.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $latestVersion;
|
||||
|
||||
/**
|
||||
* Where to get information of new application versions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $updateSource;
|
||||
|
||||
/**
|
||||
* Updates information fetched from update source.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $updateInfo;
|
||||
|
||||
/**
|
||||
* Guzzle HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $guzzle;
|
||||
|
||||
/**
|
||||
* Default request options for Guzzle HTTP client.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guzzleConfig;
|
||||
|
||||
public function __construct(\GuzzleHttp\Client $guzzle)
|
||||
{
|
||||
$this->updateSource = config('app.update_source');
|
||||
$this->currentVersion = config('app.version');
|
||||
|
||||
$this->guzzle = $guzzle;
|
||||
$this->guzzleConfig = [
|
||||
'headers' => ['User-Agent' => config('secure.user_agent')],
|
||||
'verify' => config('secure.certificates')
|
||||
];
|
||||
}
|
||||
|
||||
public function showUpdatePage()
|
||||
{
|
||||
$info = $this->getUpdateInfo();
|
||||
$canUpdate = $this->canUpdate(Arr::get($info, 'info'));
|
||||
$info = [
|
||||
'latest_version' => '',
|
||||
'current_version' => $this->currentVersion,
|
||||
'release_note' => '',
|
||||
'release_url' => '',
|
||||
'pre_release' => false,
|
||||
// Fallback to current time
|
||||
'release_time' => '',
|
||||
'new_version_available' => false
|
||||
];
|
||||
|
||||
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'],
|
||||
// If current update source is available
|
||||
if ($this->getUpdateInfo()) {
|
||||
$info['latest_version'] = $this->getUpdateInfo('latest_version');
|
||||
|
||||
$info['new_version_available'] = Comparator::greaterThan(
|
||||
$info['latest_version'],
|
||||
$info['current_version']
|
||||
);
|
||||
|
||||
if ($detail = $this->getReleaseInfo($info['latest_version'])) {
|
||||
$info = array_merge($info, array_only($detail, [
|
||||
'release_note',
|
||||
'release_url',
|
||||
'release_time',
|
||||
'pre_release'
|
||||
]));
|
||||
} else {
|
||||
// if detailed release info is not given
|
||||
$info['new_version_available'] = false;
|
||||
}
|
||||
|
||||
if (! $info['new_version_available']) {
|
||||
$info['release_time'] = array_get($this->getReleaseInfo($this->currentVersion), 'release_time');
|
||||
}
|
||||
}
|
||||
|
||||
$connectivity = true;
|
||||
|
||||
try {
|
||||
$this->guzzle->request('GET', $this->updateSource, $this->guzzleConfig);
|
||||
} catch (Exception $e) {
|
||||
$connectivity = $e->getMessage();
|
||||
}
|
||||
|
||||
return view('admin.update', compact('info', 'connectivity'));
|
||||
}
|
||||
|
||||
public function checkUpdates()
|
||||
{
|
||||
return json([
|
||||
'latest' => $this->getUpdateInfo('latest_version'),
|
||||
'available' => $this->newVersionAvailable()
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(Unzip $unzip, Filesystem $filesystem)
|
||||
protected function newVersionAvailable()
|
||||
{
|
||||
$info = $this->getUpdateInfo();
|
||||
if (!$info['ok'] || !$this->canUpdate($info['info'])['can']) {
|
||||
return json(trans('admin.update.info.up-to-date'), 1);
|
||||
}
|
||||
$latest = $this->getUpdateInfo('latest_version');
|
||||
|
||||
$info = $info['info'];
|
||||
$path = tempnam(sys_get_temp_dir(), 'bs');
|
||||
return Comparator::greaterThan($latest, $this->currentVersion) && $this->getReleaseInfo($latest);
|
||||
}
|
||||
|
||||
$response = Http::withOptions([
|
||||
'sink' => $path,
|
||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||
])->get($info['url']);
|
||||
public function download(Request $request)
|
||||
{
|
||||
if (! $this->newVersionAvailable())
|
||||
return;
|
||||
|
||||
if ($response->ok()) {
|
||||
$unzip->extract($path, base_path());
|
||||
$action = $request->get('action');
|
||||
$release_url = $this->getReleaseInfo($this->latestVersion)['release_url'];
|
||||
$tmp_path = Cache::get('tmp_path');
|
||||
|
||||
// Delete options cache. This allows us to update the version.
|
||||
$filesystem->delete(storage_path('options.php'));
|
||||
switch ($action) {
|
||||
case 'prepare-download':
|
||||
|
||||
return json(trans('admin.update.complete'), 0);
|
||||
} else {
|
||||
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
|
||||
Cache::forget('download-progress');
|
||||
$update_cache = storage_path('update_cache');
|
||||
|
||||
if (! is_dir($update_cache)) {
|
||||
if (false === Storage::disk('root')->makeDirectory('storage/update_cache')) {
|
||||
return response(trans('admin.update.errors.write-permission'));
|
||||
}
|
||||
}
|
||||
|
||||
// Set temporary path for the update package
|
||||
$tmp_path = $update_cache.'/update_'.time().'.zip';
|
||||
Cache::put('tmp_path', $tmp_path, 60);
|
||||
Log::info('[UpdateWizard] Prepare to download update package', compact('release_url', 'tmp_path'));
|
||||
|
||||
// We won't get remote file size here since HTTP HEAD method is not always reliable
|
||||
return json(compact('release_url', 'tmp_path'));
|
||||
|
||||
case 'start-download':
|
||||
|
||||
if (! $tmp_path) {
|
||||
return 'No temp path available, please try again.';
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
$GLOBALS['last_downloaded'] = 0;
|
||||
|
||||
Log::info('[UpdateWizard] Start downloading update package');
|
||||
|
||||
try {
|
||||
$this->guzzle->request('GET', $release_url, array_merge($this->guzzleConfig, [
|
||||
'sink' => $tmp_path,
|
||||
'progress' => function ($total, $downloaded) {
|
||||
if ($total == 0) return;
|
||||
// Log current progress per 100 KiB
|
||||
if ($total == $downloaded || floor($downloaded / 102400) > floor($GLOBALS['last_downloaded'] / 102400)) {
|
||||
$GLOBALS['last_downloaded'] = $downloaded;
|
||||
Log::info('[UpdateWizard] Download progress (in bytes):', [$total, $downloaded]);
|
||||
Cache::put('download-progress', compact('total', 'downloaded'), 60);
|
||||
}
|
||||
}
|
||||
]));
|
||||
} catch (Exception $e) {
|
||||
@unlink($tmp_path);
|
||||
return response(trans('admin.update.errors.prefix').$e->getMessage());
|
||||
}
|
||||
|
||||
Log::info('[UpdateWizard] Finished downloading update package');
|
||||
|
||||
return json(compact('tmp_path'));
|
||||
|
||||
case 'get-progress':
|
||||
|
||||
return json((array) Cache::get('download-progress'));
|
||||
|
||||
case 'extract':
|
||||
|
||||
if (! file_exists($tmp_path)) {
|
||||
return response('No file available');
|
||||
}
|
||||
|
||||
$extract_dir = storage_path("update_cache/{$this->latestVersion}");
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$res = $zip->open($tmp_path);
|
||||
|
||||
if ($res === true) {
|
||||
Log::info("[UpdateWizard] Extracting file $tmp_path");
|
||||
|
||||
if ($zip->extractTo($extract_dir) === false) {
|
||||
return response(trans('admin.update.errors.prefix').'Cannot unzip file.');
|
||||
}
|
||||
|
||||
} else {
|
||||
return response(trans('admin.update.errors.unzip').$res);
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
try {
|
||||
File::copyDirectory("$extract_dir/vendor", base_path('vendor'));
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
Log::error('[UpdateWizard] Unable to extract vendors');
|
||||
// Skip copying vendor
|
||||
File::deleteDirectory("$extract_dir/vendor");
|
||||
}
|
||||
|
||||
try {
|
||||
File::copyDirectory($extract_dir, base_path());
|
||||
|
||||
Log::info('[UpdateWizard] Overwrite with extracted files');
|
||||
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
Log::error('[UpdateWizard] Error occured when overwriting files');
|
||||
|
||||
// Response can be returned, while cache will be cleared
|
||||
// @see https://gist.github.com/g-plane/2f88ad582826a78e0a26c33f4319c1e0
|
||||
return response(trans('admin.update.errors.overwrite').$e->getMessage());
|
||||
} finally {
|
||||
File::deleteDirectory(storage_path('update_cache'));
|
||||
Log::info('[UpdateWizard] Cleaning cache');
|
||||
}
|
||||
|
||||
Log::info('[UpdateWizard] Done');
|
||||
return json(trans('admin.update.complete'), 0);
|
||||
|
||||
default:
|
||||
return json(trans('general.illegal-parameters'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getUpdateInfo()
|
||||
protected function getUpdateInfo($key = null)
|
||||
{
|
||||
$response = Http::withOptions([
|
||||
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||
])->get(config('app.update_source'));
|
||||
if (! $this->updateInfo) {
|
||||
// Add timestamp to control cdn cache
|
||||
$url = starts_with($this->updateSource, 'http')
|
||||
? $this->updateSource."?v=".substr(time(), 0, -3)
|
||||
: $this->updateSource;
|
||||
|
||||
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')];
|
||||
try {
|
||||
$response = $this->guzzle->request('GET', $url, $this->guzzleConfig)->getBody();
|
||||
} catch (Exception $e) {
|
||||
Log::error("[CheckingUpdate] Failed to get update information: ".$e->getMessage());
|
||||
}
|
||||
|
||||
if (isset($response)) {
|
||||
$this->updateInfo = json_decode($response, true);
|
||||
}
|
||||
} else {
|
||||
return ['ok' => false, 'error' => 'HTTP status code: '.$response->status()];
|
||||
}
|
||||
|
||||
$this->latestVersion = array_get($this->updateInfo, 'latest_version', $this->currentVersion);
|
||||
|
||||
if (! is_null($key)) {
|
||||
return array_get($this->updateInfo, $key);
|
||||
}
|
||||
|
||||
return $this->updateInfo;
|
||||
}
|
||||
|
||||
protected function canUpdate($info = [])
|
||||
protected function getReleaseInfo($version)
|
||||
{
|
||||
$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' => ''];
|
||||
return array_get($this->getUpdateInfo('releases'), $version);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,145 +2,109 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\UserProfileUpdated;
|
||||
use App\Mail\EmailVerification;
|
||||
use App\Models\Texture;
|
||||
use App;
|
||||
use Mail;
|
||||
use View;
|
||||
use Schema;
|
||||
use Session;
|
||||
use App\Models\User;
|
||||
use Blessing\Filter;
|
||||
use Blessing\Rejection;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use App\Models\Texture;
|
||||
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;
|
||||
use App\Events\UserProfileUpdated;
|
||||
use App\Exceptions\PrettyPageException;
|
||||
use App\Services\Repositories\UserRepository;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function user()
|
||||
{
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
/**
|
||||
* Current user instance.
|
||||
*
|
||||
* @var App\Models\User
|
||||
*/
|
||||
private $user = null;
|
||||
|
||||
return $user
|
||||
->makeHidden(['password', 'ip', 'remember_token', 'verification_token']);
|
||||
public function __construct(UserRepository $users)
|
||||
{
|
||||
$this->user = $users->get(session('uid'));
|
||||
|
||||
// Do nothing if new columns are not ready
|
||||
if (Schema::hasColumn('users', 'verified') && option('require_verification')) {
|
||||
// Send email verification link to newly registered users
|
||||
$this->user->verification_token || $this->sendVerificationEmail();
|
||||
}
|
||||
}
|
||||
|
||||
public function index(Filter $filter)
|
||||
public function index()
|
||||
{
|
||||
$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],
|
||||
'user' => $this->user,
|
||||
'statistics' => [
|
||||
'players' => $this->calculatePercentageUsed($this->user->players->count(), option('score_per_player')),
|
||||
'storage' => $this->calculatePercentageUsed($this->user->getStorageUsed(), option('score_per_storage'))
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function scoreInfo()
|
||||
/**
|
||||
* Calculate percentage of resources used by user.
|
||||
*
|
||||
* @param int $used
|
||||
* @param int $rate
|
||||
* @return array
|
||||
*/
|
||||
protected function calculatePercentageUsed($used, $rate)
|
||||
{
|
||||
/** @var User */
|
||||
$user = Auth::user();
|
||||
// Initialize default value to avoid division by zero.
|
||||
$result['used'] = $used;
|
||||
$result['total'] = 'UNLIMITED';
|
||||
$result['percentage'] = 0;
|
||||
|
||||
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);
|
||||
if ($rate != 0) {
|
||||
$result['total'] = $used + floor($this->user->getScore() / $rate);
|
||||
$result['percentage'] = $result['total'] ? $used / $result['total'] * 100 : 100;
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($remainingTime <= 0) {
|
||||
[$min, $max] = explode(',', option('sign_score'));
|
||||
$acquiredScore = rand((int) $min, (int) $max);
|
||||
$acquiredScore = $filter->apply('sign_score', $acquiredScore);
|
||||
/**
|
||||
* Handle user signing.
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function sign()
|
||||
{
|
||||
if ($this->user->canSign()) {
|
||||
$acquiredScore = $this->user->sign();
|
||||
|
||||
$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,
|
||||
return json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('user.sign-success', ['score' => $acquiredScore]),
|
||||
'score' => $this->user->getScore(),
|
||||
'storage' => $this->calculatePercentageUsed($this->user->getStorageUsed(), option('score_per_storage')),
|
||||
'remaining_time' => $this->getUserSignRemainingTimeWithPrecision()
|
||||
]);
|
||||
} else {
|
||||
return json('', 1);
|
||||
$remaining_time = $this->getUserSignRemainingTimeWithPrecision();
|
||||
return json(trans('user.cant-sign-until', [
|
||||
'time' => $remaining_time >= 1
|
||||
? $remaining_time : round($remaining_time * 60),
|
||||
'unit' => $remaining_time >= 1
|
||||
? trans('user.time-unit-hour') : trans('user.time-unit-min')
|
||||
]), 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserSignRemainingTimeWithPrecision()
|
||||
{
|
||||
$hours = $this->user->getSignRemainingTime() / 3600;
|
||||
|
||||
return $hours > 1 ? round($hours) : $hours;
|
||||
}
|
||||
|
||||
public function sendVerificationEmail()
|
||||
{
|
||||
if (!option('require_verification')) {
|
||||
if (!option('require_verification') || !Schema::hasColumn('users', 'verified')) {
|
||||
return json(trans('user.verification.disabled'), 1);
|
||||
}
|
||||
|
||||
|
|
@ -151,17 +115,26 @@ class UserController extends Controller
|
|||
return json(trans('user.verification.frequent-mail'), 1);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->verified) {
|
||||
if ($this->user->verified) {
|
||||
return json(trans('user.verification.verified'), 1);
|
||||
}
|
||||
|
||||
$url = URL::signedRoute('auth.verify', ['user' => $user], null, false);
|
||||
$token = generate_random_token();
|
||||
$this->user->verification_token = $token;
|
||||
$this->user->save();
|
||||
|
||||
$email = $this->user->email;
|
||||
$url = option('site_url')."/auth/verify?uid={$this->user->uid}&token=$token";
|
||||
|
||||
try {
|
||||
Mail::to($user->email)->send(new EmailVerification(url($url)));
|
||||
Mail::send('mails.email-verification', compact('url'), function ($m) use ($email) {
|
||||
$site_name = option_localized('site_name');
|
||||
|
||||
$m->from(config('mail.username'), $site_name);
|
||||
$m->to($email)->subject(trans('user.verification.mail.title', ['sitename' => $site_name]));
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
// Write the exception to log
|
||||
report($e);
|
||||
|
||||
return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2);
|
||||
|
|
@ -172,189 +145,137 @@ class UserController extends Controller
|
|||
return json(trans('user.verification.success'), 0);
|
||||
}
|
||||
|
||||
public function profile(Filter $filter)
|
||||
public function profile()
|
||||
{
|
||||
$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'));
|
||||
return view('user.profile')->with('user', $this->user);
|
||||
}
|
||||
|
||||
public function handleProfile(Request $request, Filter $filter, Dispatcher $dispatcher)
|
||||
/**
|
||||
* Handle changing user profile.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param UserRepository $users
|
||||
* @return mixed
|
||||
*/
|
||||
public function handleProfile(Request $request, UserRepository $users)
|
||||
{
|
||||
$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']);
|
||||
$this->validate($request, [
|
||||
'new_nickname' => 'required|no_special_chars|max:255'
|
||||
]);
|
||||
|
||||
$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);
|
||||
if ($this->user->setNickName($nickname)) {
|
||||
event(new UserProfileUpdated($action, $this->user));
|
||||
return json(trans('user.profile.nickname.success', ['nickname' => $nickname]), 0);
|
||||
}
|
||||
|
||||
$user->changePassword($request->input('new_password'));
|
||||
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
|
||||
event(new UserProfileUpdated($action, $user));
|
||||
break; // @codeCoverageIgnore
|
||||
|
||||
Auth::logout();
|
||||
|
||||
return json(trans('user.profile.password.success'), 0);
|
||||
|
||||
case 'email':
|
||||
$data = $request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:6|max:32',
|
||||
case 'password':
|
||||
$this->validate($request, [
|
||||
'current_password' => 'required|min:6|max:32',
|
||||
'new_password' => 'required|min:8|max:32'
|
||||
]);
|
||||
|
||||
if (User::where('email', $data['email'])->count() > 0) {
|
||||
if (! $this->user->verifyPassword($request->input('current_password')))
|
||||
return json(trans('user.profile.password.wrong-password'), 1);
|
||||
|
||||
if ($this->user->changePasswd($request->input('new_password'))) {
|
||||
event(new UserProfileUpdated($action, $this->user));
|
||||
|
||||
session()->flush();
|
||||
|
||||
return json(trans('user.profile.password.success'), 0)
|
||||
->withCookie(cookie()->forget('uid'))
|
||||
->withCookie(cookie()->forget('token'));
|
||||
}
|
||||
|
||||
break; // @codeCoverageIgnore
|
||||
|
||||
case 'email':
|
||||
$this->validate($request, [
|
||||
'new_email' => 'required|email',
|
||||
'password' => 'required|min:6|max:32'
|
||||
]);
|
||||
|
||||
if ($users->get($request->input('new_email'), 'email')) {
|
||||
return json(trans('user.profile.email.existed'), 1);
|
||||
}
|
||||
|
||||
if (!$user->verifyPassword($data['password'])) {
|
||||
if (! $this->user->verifyPassword($request->input('password')))
|
||||
return json(trans('user.profile.email.wrong-password'), 1);
|
||||
|
||||
if ($this->user->setEmail($request->input('new_email'))) {
|
||||
// Set account status to unverified
|
||||
$this->user->verified = false;
|
||||
$this->user->verification_token = '';
|
||||
$this->user->save();
|
||||
|
||||
event(new UserProfileUpdated($action, $this->user));
|
||||
|
||||
return json(trans('user.profile.email.success'), 0)
|
||||
->withCookie(cookie()->forget('uid'))
|
||||
->withCookie(cookie()->forget('token'));
|
||||
}
|
||||
|
||||
$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);
|
||||
break; // @codeCoverageIgnore
|
||||
|
||||
case 'delete':
|
||||
$request->validate([
|
||||
'password' => 'required|min:6|max:32',
|
||||
$this->validate($request, [
|
||||
'password' => 'required|min:6|max:32'
|
||||
]);
|
||||
|
||||
if ($user->isAdmin()) {
|
||||
return json(trans('user.profile.delete.admin'), 1);
|
||||
}
|
||||
|
||||
if (!$user->verifyPassword($request->input('password'))) {
|
||||
if (! $this->user->verifyPassword($request->input('password')))
|
||||
return json(trans('user.profile.delete.wrong-password'), 1);
|
||||
|
||||
if ($this->user->delete()) {
|
||||
session()->flush();
|
||||
|
||||
return response()
|
||||
->json([
|
||||
'errno' => 0,
|
||||
'msg' => trans('user.profile.delete.success')
|
||||
])
|
||||
->cookie('uid', '', time() - 3600, '/')
|
||||
->cookie('token', '', time() - 3600, '/');
|
||||
}
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$dispatcher->dispatch('user.deleting', [$user]);
|
||||
|
||||
$user->delete();
|
||||
$dispatcher->dispatch('user.deleted', [$user]);
|
||||
session()->flush();
|
||||
|
||||
return json(trans('user.profile.delete.success'), 0);
|
||||
break; // @codeCoverageIgnore
|
||||
|
||||
default:
|
||||
return json(trans('general.illegal-parameters'), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
public function setAvatar(Request $request, Filter $filter, Dispatcher $dispatcher)
|
||||
/**
|
||||
* Set user avatar.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function setAvatar(Request $request)
|
||||
{
|
||||
$request->validate(['tid' => 'required|integer']);
|
||||
$tid = $request->input('tid');
|
||||
/** @var User */
|
||||
$user = auth()->user();
|
||||
$this->validate($request, [
|
||||
'tid' => 'required|integer'
|
||||
]);
|
||||
|
||||
$can = $filter->apply('user_can_update_avatar', true, [$user, $tid]);
|
||||
if ($can instanceof Rejection) {
|
||||
return json($can->getReason(), 1);
|
||||
}
|
||||
$result = Texture::find($request->input('tid'));
|
||||
|
||||
$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') {
|
||||
if ($result) {
|
||||
if ($result->type == "cape")
|
||||
return json(trans('user.profile.avatar.wrong-type'), 1);
|
||||
|
||||
if ($this->user->setAvatar($request->input('tid'))) {
|
||||
return json(trans('user.profile.avatar.success'), 0);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
} // @codeCoverageIgnore
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,61 +11,46 @@ class Kernel extends HttpKernel
|
|||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array<int, class-string|string>
|
||||
* @var array
|
||||
*/
|
||||
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,
|
||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\App\Http\Middleware\RedirectIfUrlEndsWithSlash::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,
|
||||
\App\Http\Middleware\AfterSessionBooted::class,
|
||||
\App\Http\Middleware\DetectLanguagePrefer::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'authorize' => [
|
||||
'auth:web',
|
||||
Middleware\RejectBannedUser::class,
|
||||
Middleware\EnsureEmailFilled::class,
|
||||
Middleware\FireUserAuthenticated::class,
|
||||
],
|
||||
'static' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's middleware aliases.
|
||||
* The application's route middleware.
|
||||
*
|
||||
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
|
||||
* These middleware may be assigned to groups or used individually.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
* @var array
|
||||
*/
|
||||
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,
|
||||
protected $routeMiddleware = [
|
||||
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
|
||||
'auth' => \App\Http\Middleware\CheckAuthenticated::class,
|
||||
'verified' => \App\Http\Middleware\CheckUserVerified::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'admin' => \App\Http\Middleware\CheckAdministrator::class,
|
||||
'super-admin' => \App\Http\Middleware\CheckSuperAdmin::class,
|
||||
'player' => \App\Http\Middleware\CheckPlayerExist::class,
|
||||
'setup' => \App\Http\Middleware\CheckInstallation::class,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
33
app/Http/Middleware/AfterSessionBooted.php
Normal file
33
app/Http/Middleware/AfterSessionBooted.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class AfterSessionBooted
|
||||
{
|
||||
/**
|
||||
* Jobs should be done after session booted.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
static $jobs;
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
foreach (static::$jobs as $job) {
|
||||
if (is_callable($job)) {
|
||||
app()->call($job);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user