Compare commits

..

No commits in common. "dev" and "v4" have entirely different histories.
dev ... v4

1014 changed files with 44177 additions and 65883 deletions

92
.circleci/config.yml Normal file
View File

@ -0,0 +1,92 @@
version: 2
jobs:
frontend:
working_directory: ~/repo
docker:
- image: circleci/node:10
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "yarn.lock" }}
- v1-dependencies-
- run: yarn
- save_cache:
paths:
- node_modules
- ~/.yarn
key: v1-dependencies-{{ checksum "yarn.lock" }}
- run: yarn lint
- run: yarn test --coverage -w=2
- run: yarn codecov
composer:
working_directory: ~/repo
docker:
- image: blessingskin/ci:7.1
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "composer.lock" }}
- v1-dependencies-
- run: composer install -n --prefer-dist
- save_cache:
paths:
- vendor
key: v1-dependencies-{{ checksum "composer.lock" }}
- run: cp .env.testing .env
- run: php artisan key:random
- run: php artisan salt:random
- persist_to_workspace:
root: ~/repo
paths:
- .
php7.1:
working_directory: ~/repo
docker:
- image: blessingskin/ci:7.1
steps:
- attach_workspace:
at: ~/repo
- run: touch storage/testing.sqlite
- run: ./vendor/bin/phpunit --coverage-clover=coverage.xml
- run: bash <(curl -s https://codecov.io/bash) -cF php
php7.2:
working_directory: ~/repo
docker:
- image: blessingskin/ci:7.2
steps:
- attach_workspace:
at: ~/repo
- run: touch storage/testing.sqlite
- run: ./vendor/bin/phpunit
php7.3:
working_directory: ~/repo
docker:
- image: blessingskin/ci:7.3
steps:
- attach_workspace:
at: ~/repo
- run: touch storage/testing.sqlite
- run: ./vendor/bin/phpunit
workflows:
version: 2
install_and_test:
jobs:
- frontend
- composer
- php7.1:
requires:
- composer
- php7.2:
requires:
- composer
- php7.3:
requires:
- composer

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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:

View File

@ -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

View File

@ -9,5 +9,5 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{php,md,ps1,Dockerfile}]
[*.{php,md,tpl,rs}]
indent_size = 4

View File

@ -1,45 +1,76 @@
APP_DEBUG=false
APP_ENV=production
APP_FALLBACK_LOCALE=en
###################################
# Blessing Skin Server v4 #
# Configuration #
###################################
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blessingskin
DB_USERNAME=username
DB_PASSWORD=secret
DB_PREFIX=
# 本文件中各个配置项的详细说明
# 请访问 http://t.cn/E9G4DTY 查看中文教程
# Be sure to disable debug at production environment!
APP_DEBUG = false
APP_ENV = production
# Database Configuration
DB_CONNECTION = dummy
DB_HOST = localhost
DB_PORT = 3306
DB_DATABASE = blessingskin
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
# - BCRYPT, PHP_PASSWORD_HASH
# - MD5, SALTED2MD5
# - SHA256, SALTED2SHA256
# - SHA512, SALTED2SHA512
#
# New sites are *highly* recommended to use BCRYPT.
#
PWD_METHOD=BCRYPT
APP_KEY=
PWD_METHOD = BCRYPT
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 = 2c5ca184f017a9a1ffbd198ef69b0c0e
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
# App Key should be setted to any random, *32 character* string,
# otherwise all the encrypted strings will not be safe.
#
# You can run [php artisan key:generate] to generate a new key.
#
APP_KEY = base64:gkb/zouNF6UOSfnr/o+izVMS57WQS3+62YqZBuDyBhU=
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 = null
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
REDIS_HOST = 127.0.0.1
REDIS_PASSWORD = null
REDIS_PORT = 6379
PLUGINS_DIR = null
PLUGINS_URL = null

732
.env.laradock Normal file
View File

@ -0,0 +1,732 @@
###########################################################
###################### General Setup ######################
###########################################################
### Paths #################################################
# Point to the path of your applications code on your host
APP_CODE_PATH_HOST=../
# Point to where the `APP_CODE_PATH_HOST` should be in the container
APP_CODE_PATH_CONTAINER=/var/www
# You may add flags to the path `:cached`, `:delegated`. When using Docker Sync add `:nocopy`
APP_CODE_CONTAINER_FLAG=:cached
# Choose storage path on your machine. For all storage systems
DATA_PATH_HOST=~/.laradock/data
### Drivers ################################################
# All volumes driver
VOLUMES_DRIVER=local
# All Networks driver
NETWORKS_DRIVER=bridge
### Docker compose files ##################################
# Select which docker-compose files to include. If using docker-sync append `:docker-compose.sync.yml` at the end
COMPOSE_FILE=docker-compose.yml
# Change the separator from : to ; on Windows
COMPOSE_PATH_SEPARATOR=:
# Define the prefix of container names. This is useful if you have multiple projects that use laradock to have seperate containers per project.
COMPOSE_PROJECT_NAME=laradock
### PHP Version ###########################################
# Select a PHP version of the Workspace and PHP-FPM containers (Does not apply to HHVM). Accepted values: 7.3 - 7.2 - 7.1 - 7.0 - 5.6
PHP_VERSION=7.3
### Phalcon Version ###########################################
# Select a Phalcon version of the Workspace and PHP-FPM containers (Does not apply to HHVM). Accepted values: 3.4.0+
PHALCON_VERSION=3.4.1
### PHP Interpreter #######################################
# Select the PHP Interpreter. Accepted values: hhvm - php-fpm
PHP_INTERPRETER=php-fpm
### Docker Host IP ########################################
# Enter your Docker Host IP (will be appended to /etc/hosts). Default is `10.0.75.1`
DOCKER_HOST_IP=10.0.75.1
### Remote Interpreter ####################################
# Choose a Remote Interpreter entry matching name. Default is `laradock`
PHP_IDE_CONFIG=serverName=laradock
### Windows Path ##########################################
# A fix for Windows users, to ensure the application path works
COMPOSE_CONVERT_WINDOWS_PATHS=1
### Environment ###########################################
# If you need to change the sources (i.e. to China), set CHANGE_SOURCE to true
CHANGE_SOURCE=true
### Docker Sync ###########################################
# If you are using Docker Sync. For `osx` use 'native_osx', for `windows` use 'unison', for `linux` docker-sync is not required
DOCKER_SYNC_STRATEGY=native_osx
###########################################################
################ Containers Customization #################
###########################################################
### WORKSPACE #############################################
WORKSPACE_COMPOSER_GLOBAL_INSTALL=true
WORKSPACE_COMPOSER_AUTH=false
WORKSPACE_COMPOSER_REPO_PACKAGIST=
WORKSPACE_INSTALL_NODE=false
WORKSPACE_NODE_VERSION=node
WORKSPACE_NPM_REGISTRY=
WORKSPACE_INSTALL_YARN=false
WORKSPACE_YARN_VERSION=latest
WORKSPACE_INSTALL_NPM_GULP=false
WORKSPACE_INSTALL_NPM_BOWER=false
WORKSPACE_INSTALL_NPM_VUE_CLI=false
WORKSPACE_INSTALL_NPM_ANGULAR_CLI=false
WORKSPACE_INSTALL_PHPREDIS=true
WORKSPACE_INSTALL_WORKSPACE_SSH=false
WORKSPACE_INSTALL_SUBVERSION=false
WORKSPACE_INSTALL_XDEBUG=false
WORKSPACE_INSTALL_PHPDBG=false
WORKSPACE_INSTALL_SSH2=false
WORKSPACE_INSTALL_LDAP=false
WORKSPACE_INSTALL_GMP=false
WORKSPACE_INSTALL_SOAP=false
WORKSPACE_INSTALL_XSL=false
WORKSPACE_INSTALL_IMAP=false
WORKSPACE_INSTALL_MONGO=false
WORKSPACE_INSTALL_AMQP=false
WORKSPACE_INSTALL_MSSQL=false
WORKSPACE_INSTALL_DRUSH=false
WORKSPACE_DRUSH_VERSION=8.1.17
WORKSPACE_INSTALL_DRUPAL_CONSOLE=false
WORKSPACE_INSTALL_WP_CLI=false
WORKSPACE_INSTALL_AEROSPIKE=false
WORKSPACE_INSTALL_V8JS=false
WORKSPACE_INSTALL_LARAVEL_ENVOY=false
WORKSPACE_INSTALL_LARAVEL_INSTALLER=false
WORKSPACE_INSTALL_DEPLOYER=false
WORKSPACE_INSTALL_PRESTISSIMO=false
WORKSPACE_INSTALL_LINUXBREW=false
WORKSPACE_INSTALL_MC=false
WORKSPACE_INSTALL_SYMFONY=false
WORKSPACE_INSTALL_PYTHON=false
WORKSPACE_INSTALL_POWERLINE=false
WORKSPACE_INSTALL_IMAGE_OPTIMIZERS=false
WORKSPACE_INSTALL_IMAGEMAGICK=false
WORKSPACE_INSTALL_TERRAFORM=false
WORKSPACE_INSTALL_DUSK_DEPS=false
WORKSPACE_INSTALL_PG_CLIENT=false
WORKSPACE_INSTALL_PHALCON=false
WORKSPACE_INSTALL_SWOOLE=false
WORKSPACE_INSTALL_TAINT=false
WORKSPACE_INSTALL_LIBPNG=false
WORKSPACE_INSTALL_IONCUBE=false
WORKSPACE_INSTALL_MYSQL_CLIENT=false
WORKSPACE_INSTALL_PING=false
WORKSPACE_INSTALL_SSHPASS=false
WORKSPACE_INSTALL_INOTIFY=false
WORKSPACE_INSTALL_FSWATCH=false
WORKSPACE_PUID=1000
WORKSPACE_PGID=1000
WORKSPACE_CHROME_DRIVER_VERSION=2.42
WORKSPACE_TIMEZONE=UTC
WORKSPACE_SSH_PORT=2222
WORKSPACE_INSTALL_FFMPEG=false
WORKSPACE_INSTALL_GNU_PARALLEL=false
### PHP_FPM ###############################################
PHP_FPM_INSTALL_BCMATH=true
PHP_FPM_INSTALL_MYSQLI=true
PHP_FPM_INSTALL_INTL=true
PHP_FPM_INSTALL_IMAGEMAGICK=true
PHP_FPM_INSTALL_OPCACHE=true
PHP_FPM_INSTALL_IMAGE_OPTIMIZERS=true
PHP_FPM_INSTALL_PHPREDIS=true
PHP_FPM_INSTALL_MEMCACHED=false
PHP_FPM_INSTALL_XDEBUG=false
PHP_FPM_INSTALL_XHPROF=false
PHP_FPM_INSTALL_PHPDBG=false
PHP_FPM_INSTALL_IMAP=false
PHP_FPM_INSTALL_MONGO=false
PHP_FPM_INSTALL_AMQP=false
PHP_FPM_INSTALL_MSSQL=false
PHP_FPM_INSTALL_SSH2=false
PHP_FPM_INSTALL_SOAP=false
PHP_FPM_INSTALL_XSL=false
PHP_FPM_INSTALL_GMP=false
PHP_FPM_INSTALL_EXIF=false
PHP_FPM_INSTALL_AEROSPIKE=false
PHP_FPM_INSTALL_PGSQL=false
PHP_FPM_INSTALL_GHOSTSCRIPT=false
PHP_FPM_INSTALL_LDAP=false
PHP_FPM_INSTALL_PHALCON=false
PHP_FPM_INSTALL_SWOOLE=false
PHP_FPM_INSTALL_TAINT=false
PHP_FPM_INSTALL_PG_CLIENT=false
PHP_FPM_INSTALL_POSTGIS=false
PHP_FPM_INSTALL_PCNTL=false
PHP_FPM_INSTALL_CALENDAR=false
PHP_FPM_INSTALL_FAKETIME=false
PHP_FPM_INSTALL_IONCUBE=false
PHP_FPM_INSTALL_RDKAFKA=false
PHP_FPM_FAKETIME=-0
PHP_FPM_INSTALL_APCU=false
PHP_FPM_INSTALL_YAML=false
PHP_FPM_INSTALL_ADDITIONAL_LOCALES=false
PHP_FPM_INSTALL_MYSQL_CLIENT=false
PHP_FPM_INSTALL_PING=false
PHP_FPM_INSTALL_SSHPASS=false
PHP_FPM_FFMPEG=false
PHP_FPM_ADDITIONAL_LOCALES="es_ES.UTF-8 fr_FR.UTF-8"
### PHP_WORKER ############################################
PHP_WORKER_INSTALL_PGSQL=false
PHP_WORKER_INSTALL_BCMATH=false
PHP_WORKER_INSTALL_PHALCON=false
PHP_WORKER_INSTALL_SOAP=false
PHP_WORKER_INSTALL_ZIP_ARCHIVE=false
PHP_WORKER_INSTALL_MYSQL_CLIENT=false
PHP_WORKER_INSTALL_AMQP=false
PHP_WORKER_INSTALL_GHOSTSCRIPT=false
PHP_WORKER_INSTALL_SWOOLE=false
PHP_WORKER_INSTALL_TAINT=false
PHP_WORKER_INSTALL_FFMPEG=false
PHP_WORKER_INSTALL_GMP=false
PHP_WORKER_PUID=1000
PHP_WORKER_PGID=1000
### NGINX #################################################
NGINX_HOST_HTTP_PORT=80
NGINX_HOST_HTTPS_PORT=443
NGINX_HOST_LOG_PATH=./logs/nginx/
NGINX_SITES_PATH=./nginx/sites/
NGINX_PHP_UPSTREAM_CONTAINER=php-fpm
NGINX_PHP_UPSTREAM_PORT=9000
NGINX_SSL_PATH=./nginx/ssl/
### APACHE ################################################
APACHE_HOST_HTTP_PORT=80
APACHE_HOST_HTTPS_PORT=443
APACHE_HOST_LOG_PATH=./logs/apache2
APACHE_SITES_PATH=./apache2/sites
APACHE_PHP_UPSTREAM_CONTAINER=php-fpm
APACHE_PHP_UPSTREAM_PORT=9000
APACHE_PHP_UPSTREAM_TIMEOUT=60
APACHE_DOCUMENT_ROOT=/var/www/
### MYSQL #################################################
MYSQL_VERSION=latest
MYSQL_DATABASE=default
MYSQL_USER=default
MYSQL_PASSWORD=secret
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=root
MYSQL_ENTRYPOINT_INITDB=./mysql/docker-entrypoint-initdb.d
### REDIS #################################################
REDIS_PORT=6379
### REDIS CLUSTER #########################################
REDIS_CLUSTER_PORT_RANGE=7000-7005
### ZooKeeper #############################################
ZOOKEEPER_PORT=2181
### Percona ###############################################
PERCONA_DATABASE=homestead
PERCONA_USER=homestead
PERCONA_PASSWORD=secret
PERCONA_PORT=3306
PERCONA_ROOT_PASSWORD=root
PERCONA_ENTRYPOINT_INITDB=./percona/docker-entrypoint-initdb.d
### MSSQL #################################################
MSSQL_DATABASE=homestead
MSSQL_PASSWORD=yourStrong(!)Password
MSSQL_PORT=1433
### MARIADB ###############################################
MARIADB_DATABASE=default
MARIADB_USER=default
MARIADB_PASSWORD=secret
MARIADB_PORT=3306
MARIADB_ROOT_PASSWORD=root
MARIADB_ENTRYPOINT_INITDB=./mariadb/docker-entrypoint-initdb.d
### POSTGRES ##############################################
POSTGRES_DB=default
POSTGRES_USER=default
POSTGRES_PASSWORD=secret
POSTGRES_PORT=5432
POSTGRES_ENTRYPOINT_INITDB=./postgres/docker-entrypoint-initdb.d
### RABBITMQ ##############################################
RABBITMQ_NODE_HOST_PORT=5672
RABBITMQ_MANAGEMENT_HTTP_HOST_PORT=15672
RABBITMQ_MANAGEMENT_HTTPS_HOST_PORT=15671
RABBITMQ_DEFAULT_USER=guest
RABBITMQ_DEFAULT_PASS=guest
### ELASTICSEARCH #########################################
ELASTICSEARCH_HOST_HTTP_PORT=9200
ELASTICSEARCH_HOST_TRANSPORT_PORT=9300
### KIBANA ################################################
KIBANA_HTTP_PORT=5601
### MEMCACHED #############################################
MEMCACHED_HOST_PORT=11211
### BEANSTALKD CONSOLE ####################################
BEANSTALKD_CONSOLE_BUILD_PATH=./beanstalkd-console
BEANSTALKD_CONSOLE_CONTAINER_NAME=beanstalkd-console
BEANSTALKD_CONSOLE_HOST_PORT=2080
### BEANSTALKD ############################################
BEANSTALKD_HOST_PORT=11300
### SELENIUM ##############################################
SELENIUM_PORT=4444
### MINIO #################################################
MINIO_PORT=9000
### ADMINER ###############################################
ADM_PORT=8080
ADM_INSTALL_MSSQL=false
### PHP MY ADMIN ##########################################
# Accepted values: mariadb - mysql
PMA_DB_ENGINE=mariadb
# Credentials/Port:
PMA_USER=default
PMA_PASSWORD=secret
PMA_ROOT_PASSWORD=secret
PMA_PORT=8080
### MAILDEV ###############################################
MAILDEV_HTTP_PORT=1080
MAILDEV_SMTP_PORT=25
### VARNISH ###############################################
VARNISH_CONFIG=/etc/varnish/default.vcl
VARNISH_PORT=8080
VARNISH_BACKEND_PORT=8888
VARNISHD_PARAMS=-p default_ttl=3600 -p default_grace=3600
### Varnish ###############################################
# Proxy 1
VARNISH_PROXY1_CACHE_SIZE=128m
VARNISH_PROXY1_BACKEND_HOST=workspace
VARNISH_PROXY1_SERVER=SERVER1
# Proxy 2
VARNISH_PROXY2_CACHE_SIZE=128m
VARNISH_PROXY2_BACKEND_HOST=workspace
VARNISH_PROXY2_SERVER=SERVER2
### HAPROXY ###############################################
HAPROXY_HOST_HTTP_PORT=8085
### JENKINS ###############################################
JENKINS_HOST_HTTP_PORT=8090
JENKINS_HOST_SLAVE_AGENT_PORT=50000
JENKINS_HOME=./jenkins/jenkins_home
### CONFLUENCE ###############################################
CONFLUENCE_POSTGRES_INIT=true
CONFLUENCE_VERSION=6.13-ubuntu-18.04-adoptopenjdk8
CONFLUENCE_POSTGRES_DB=laradock_confluence
CONFLUENCE_POSTGRES_USER=laradock_confluence
CONFLUENCE_POSTGRES_PASSWORD=laradock_confluence
CONFLUENCE_HOST_HTTP_PORT=8090
### GRAFANA ###############################################
GRAFANA_PORT=3000
### GRAYLOG ###############################################
# password must be 16 characters long
GRAYLOG_PASSWORD=somesupersecretpassword
# sha256 representation of the password
GRAYLOG_SHA256_PASSWORD=b1cb6e31e172577918c9e7806c572b5ed8477d3f57aa737bee4b5b1db3696f09
GRAYLOG_PORT=9000
GRAYLOG_SYSLOG_TCP_PORT=514
GRAYLOG_SYSLOG_UDP_PORT=514
GRAYLOG_GELF_TCP_PORT=12201
GRAYLOG_GELF_UDP_PORT=12201
### BLACKFIRE #############################################
# Create an account on blackfire.io. Don't enable blackfire and xDebug at the same time. # visit https://blackfire.io/docs/24-days/06-installation#install-probe-debian for more info.
INSTALL_BLACKFIRE=false
BLACKFIRE_CLIENT_ID=<client_id>
BLACKFIRE_CLIENT_TOKEN=<client_token>
BLACKFIRE_SERVER_ID=<server_id>
BLACKFIRE_SERVER_TOKEN=<server_token>
### AEROSPIKE #############################################
AEROSPIKE_SERVICE_PORT=3000
AEROSPIKE_FABRIC_PORT=3001
AEROSPIKE_HEARTBEAT_PORT=3002
AEROSPIKE_INFO_PORT=3003
AEROSPIKE_STORAGE_GB=1
AEROSPIKE_MEM_GB=1
AEROSPIKE_NAMESPACE=test
### RETHINKDB #############################################
RETHINKDB_PORT=8090
### MONGODB ###############################################
MONGODB_PORT=27017
### CADDY #################################################
CADDY_HOST_HTTP_PORT=80
CADDY_HOST_HTTPS_PORT=443
CADDY_HOST_LOG_PATH=./logs/caddy
CADDY_CONFIG_PATH=./caddy/caddy
### LARAVEL ECHO SERVER ###################################
LARAVEL_ECHO_SERVER_PORT=6001
### THUMBOR ############################################################################################################
THUMBOR_PORT=8000
THUMBOR_LOG_FORMAT="%(asctime)s %(name)s:%(levelname)s %(message)s"
THUMBOR_LOG_DATE_FORMAT="%Y-%m-%d %H:%M:%S"
MAX_WIDTH=0
MAX_HEIGHT=0
MIN_WIDTH=1
MIN_HEIGHT=1
ALLOWED_SOURCES=[]
QUALITY=80
WEBP_QUALITY=None
PNG_COMPRESSION_LEVEL=6
AUTO_WEBP=False
MAX_AGE=86400
MAX_AGE_TEMP_IMAGE=0
RESPECT_ORIENTATION=False
IGNORE_SMART_ERRORS=False
PRESERVE_EXIF_INFO=False
ALLOW_ANIMATED_GIFS=True
USE_GIFSICLE_ENGINE=False
USE_BLACKLIST=False
LOADER=thumbor.loaders.http_loader
STORAGE=thumbor.storages.file_storage
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
RESULT_STORAGE=thumbor.result_storages.file_storage
ENGINE=thumbor.engines.pil
SECURITY_KEY="MY_SECURE_KEY"
ALLOW_UNSAFE_URL=True
ALLOW_OLD_URLS=True
FILE_LOADER_ROOT_PATH=/data/loader
HTTP_LOADER_CONNECT_TIMEOUT=5
HTTP_LOADER_REQUEST_TIMEOUT=20
HTTP_LOADER_FOLLOW_REDIRECTS=True
HTTP_LOADER_MAX_REDIRECTS=5
HTTP_LOADER_FORWARD_USER_AGENT=False
HTTP_LOADER_DEFAULT_USER_AGENT="Thumbor/5.2.1"
HTTP_LOADER_PROXY_HOST=None
HTTP_LOADER_PROXY_PORT=None
HTTP_LOADER_PROXY_USERNAME=None
HTTP_LOADER_PROXY_PASSWORD=None
HTTP_LOADER_CA_CERTS=None
HTTP_LOADER_VALIDATE_CERTS=True
HTTP_LOADER_CLIENT_KEY=None
HTTP_LOADER_CLIENT_CERT=None
HTTP_LOADER_CURL_ASYNC_HTTP_CLIENT=False
STORAGE_EXPIRATION_SECONDS=2592000
STORES_CRYPTO_KEY_FOR_EACH_IMAGE=False
FILE_STORAGE_ROOT_PATH=/data/storage
UPLOAD_MAX_SIZE=0
UPLOAD_ENABLED=False
UPLOAD_PHOTO_STORAGE=thumbor.storages.file_storage
UPLOAD_DELETE_ALLOWED=False
UPLOAD_PUT_ALLOWED=False
UPLOAD_DEFAULT_FILENAME=image
MONGO_STORAGE_SERVER_HOST=mongo
MONGO_STORAGE_SERVER_PORT=27017
MONGO_STORAGE_SERVER_DB=thumbor
MONGO_STORAGE_SERVER_COLLECTION=images
REDIS_STORAGE_SERVER_HOST=redis
REDIS_STORAGE_SERVER_PORT=6379
REDIS_STORAGE_SERVER_DB=0
REDIS_STORAGE_SERVER_PASSWORD=None
REDIS_RESULT_STORAGE_SERVER_HOST=redis
REDIS_RESULT_STORAGE_SERVER_PORT=6379
REDIS_RESULT_STORAGE_SERVER_DB=0
REDIS_RESULT_STORAGE_SERVER_PASSWORD=None
MEMCACHE_STORAGE_SERVERS=["localhost:11211",]
MIXED_STORAGE_FILE_STORAGE=thumbor.storages.no_storage
MIXED_STORAGE_CRYPTO_STORAGE=thumbor.storages.no_storage
MIXED_STORAGE_DETECTOR_STORAGE=thumbor.storages.no_storage
META_CALLBACK_NAME=None
DETECTORS=[]
FACE_DETECTOR_CASCADE_FILE=haarcascade_frontalface_alt.xml
OPTIMIZERS=[]
JPEGTRAN_PATH=/usr/bin/jpegtran
PROGRESSIVE_JPEG=True
FILTERS=["thumbor.filters.brightness", "thumbor.filters.contrast", "thumbor.filters.rgb", "thumbor.filters.round_corner", "thumbor.filters.quality", "thumbor.filters.noise", "thumbor.filters.watermark", "thumbor.filters.equalize", "thumbor.filters.fill", "thumbor.filters.sharpen", "thumbor.filters.strip_icc", "thumbor.filters.frame", "thumbor.filters.grayscale", "thumbor.filters.rotate", "thumbor.filters.format", "thumbor.filters.max_bytes", "thumbor.filters.convolution", "thumbor.filters.blur", "thumbor.filters.extract_focal", "thumbor.filters.no_upscale"]
RESULT_STORAGE_EXPIRATION_SECONDS=0
RESULT_STORAGE_FILE_STORAGE_ROOT_PATH=/data/result_storage
RESULT_STORAGE_STORES_UNSAFE=False
REDIS_QUEUE_SERVER_HOST=redis
REDIS_QUEUE_SERVER_PORT=6379
REDIS_QUEUE_SERVER_DB="0"
REDIS_QUEUE_SERVER_PASSWORD=None
SQS_QUEUE_KEY_ID=None
SQS_QUEUE_KEY_SECRET=None
SQS_QUEUE_REGION=us-east-1
USE_CUSTOM_ERROR_HANDLING=False
ERROR_HANDLER_MODULE=thumbor.error_handlers.sentry
ERROR_FILE_LOGGER=None
ERROR_FILE_NAME_USE_CONTEXT="False"
SENTRY_DSN_URL=
TC_AWS_REGION=eu-west-1
TC_AWS_ENDPOINT=None
TC_AWS_STORAGE_BUCKET=
TC_AWS_STORAGE_ROOT_PATH=
TC_AWS_LOADER_BUCKET=
TC_AWS_LOADER_ROOT_PATH=
TC_AWS_RESULT_STORAGE_BUCKET=
TC_AWS_RESULT_STORAGE_ROOT_PATH=
TC_AWS_STORAGE_SSE=False
TC_AWS_STORAGE_RRS=False
TC_AWS_ENABLE_HTTP_LOADER=False
TC_AWS_ALLOWED_BUCKETS=False
TC_AWS_STORE_METADATA=False
### SOLR ##################################################
SOLR_VERSION=5.5
SOLR_PORT=8983
SOLR_DATAIMPORTHANDLER_MYSQL=false
SOLR_DATAIMPORTHANDLER_MSSQL=false
### GITLAB ###############################################
GITLAB_POSTGRES_INIT=true
GITLAB_HOST_HTTP_PORT=8989
GITLAB_HOST_HTTPS_PORT=9898
GITLAB_HOST_SSH_PORT=2289
GITLAB_DOMAIN_NAME=http://localhost
GITLAB_ROOT_PASSWORD=laradock
GITLAB_HOST_LOG_PATH=./logs/gitlab
GITLAB_POSTGRES_HOST=postgres
GITLAB_POSTGRES_USER=laradock_gitlab
GITLAB_POSTGRES_PASSWORD=laradock_gitlab
GITLAB_POSTGRES_DB=laradock_gitlab
### GITLAB-RUNNER ###############################################
GITLAB_CI_SERVER_URL=http://localhost:8989
GITLAB_RUNNER_REGISTRATION_TOKEN=<my-registration-token>
GITLAB_REGISTER_NON_INTERACTIVE=true
### JUPYTERHUB ###############################################
JUPYTERHUB_POSTGRES_INIT=true
JUPYTERHUB_POSTGRES_HOST=postgres
JUPYTERHUB_POSTGRES_USER=laradock_jupyterhub
JUPYTERHUB_POSTGRES_PASSWORD=laradock_jupyterhub
JUPYTERHUB_POSTGRES_DB=laradock_jupyterhub
JUPYTERHUB_PORT=9991
JUPYTERHUB_OAUTH_CALLBACK_URL=http://laradock:9991/hub/oauth_callback
JUPYTERHUB_OAUTH_CLIENT_ID={GITHUB_CLIENT_ID}
JUPYTERHUB_OAUTH_CLIENT_SECRET={GITHUB_CLIENT_SECRET}
JUPYTERHUB_CUSTOM_CONFIG=./jupyterhub/jupyterhub_config.py
JUPYTERHUB_USER_DATA=/jupyterhub
JUPYTERHUB_USER_LIST=./jupyterhub/userlist
JUPYTERHUB_ENABLE_NVIDIA=false
### IPYTHON ##################################################
LARADOCK_IPYTHON_CONTROLLER_IP=127.0.0.1
### NETDATA ###############################################
NETDATA_PORT=19999
### REDISWEBUI #########################################
REDIS_WEBUI_USERNAME=laradock
REDIS_WEBUI_PASSWORD=laradock
REDIS_WEBUI_CONNECT_HOST=redis
REDIS_WEBUI_CONNECT_PORT=6379
REDIS_WEBUI_PORT=9987
### MONGOWEBUI ###############################################
MONGO_WEBUI_PORT=3000
MONGO_WEBUI_ROOT_URL=http://localhost
MONGO_WEBUI_MONGO_URL=mongodb://mongo:27017/
MONGO_WEBUI_INSTALL_MONGO=false
### METABASE ###############################################
METABASE_PORT=3030
METABASE_DB_FILE=metabase.db
METABASE_JAVA_TIMEZONE=US/Pacific
### IDE ###############################################
IDE_THEIA_PORT=987
IDE_WEBIDE_PORT=984
IDE_CODIAD_PORT=985
IDE_ICECODER_PORT=986
### DOCKERREGISTRY ###############################################
DOCKER_REGISTRY_PORT=5000
### DOCKERWEBUI ###############################################
DOCKER_WEBUI_REGISTRY_HOST=docker-registry
DOCKER_WEBUI_REGISTRY_PORT=5000
# if have use https proxy please set to 1
DOCKER_REGISTRY_USE_SSL=0
DOCKER_REGISTRY_BROWSE_ONLY=false
DOCKER_WEBUI_PORT=8754
### MAILU ###############################################
MAILU_VERSION=latest
MAILU_RECAPTCHA_PUBLIC_KEY=<YOUR_RECAPTCHA_PUBLIC_KEY>
MAILU_RECAPTCHA_PRIVATE_KEY=<YOUR_RECAPTCHA_PRIVATE_KEY>
# Main mail domain
MAILU_HTTP_PORT=6080
MAILU_HTTPS_PORT=60443
MAILU_DOMAIN=example.com
MAILU_INIT_ADMIN_USERNAME=laradock
MAILU_INIT_ADMIN_PASSWORD=laradock
# Hostnames for this server, separated with comas
MAILU_HOSTNAMES=mail.example.com,alternative.example.com,yetanother.example.com
# Postmaster local part (will append the main mail domain)
MAILU_POSTMASTER=admin
# Set to a randomly generated 16 bytes string
MAILU_SECRET_KEY=ChangeMeChangeMe
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail)
MAILU_TLS_FLAVOR=cert
# Authentication rate limit (per source IP address)
MAILU_AUTH_RATELIMIT=10/minute;1000/hour
# Opt-out of statistics, replace with "True" to opt out
MAILU_DISABLE_STATISTICS=False
# Message size limit in bytes
# Default: accept messages up to 50MB
MAILU_MESSAGE_SIZE_LIMIT=50000000
# Will relay all outgoing mails if configured
MAILU_RELAYHOST=
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
MAILU_RELAYNETS=172.16.0.0/12
# Fetchmail delay
MAILU_FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
# e.g. localpart+custom@domain;tld
MAILU_RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
MAILU_DMARC_RUA=admin
MAILU_DMARC_RUF=admin
# Welcome email, enable and set a topic and body if you wish to send welcome
# emails to all users.
MAILU_WELCOME=True
MAILU_WELCOME_SUBJECT=Welcome to your new email account
MAILU_WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!
# Path to the admin interface if enabled
MAILU_WEB_ADMIN=/admin
# Path to the webmail if enabled
MAILU_WEB_WEBMAIL=/webmail
# Website name
MAILU_SITENAME=Example Mail
# Linked Website URL
MAILU_WEBSITE=http://mail.example.com
# Default password scheme used for newly created accounts and changed passwords
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
MAILU_PASSWORD_SCHEME=SHA512-CRYPT
# Expose the admin interface (value: true, false)
MAILU_ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
MAILU_WEBMAIL=rainloop
# Dav server implementation (value: radicale, none)
MAILU_WEBDAV=radicale
### TRAEFIK #################################################
TRAEFIK_HOST_HTTP_PORT=80
TRAEFIK_HOST_HTTPS_PORT=443
### MOSQUITTO #################################################
MOSQUITTO_PORT=9001
### COUCHDB ###################################################
COUCHDB_PORT=5984
### Manticore Search ##########################################
MANTICORE_CONFIG_PATH=./manticore/config
MANTICORE_API_PORT=9312
MANTICORE_SPHINXQL_PORT=9306
MANTICORE_HTTP_PORT=9308
### pgadmin ##################################################
# use this address http://ip6-localhost:5050
PGADMIN_PORT=5050
PGADMIN_DEFAULT_EMAIL=pgadmin4@pgadmin.org
PGADMIN_DEFAULT_PASSWORD=admin
### SONARQUBE ################################################
## docker-compose up -d sonarqube
## (If you encounter a database error)
## docker-compose exec --user=root postgres
## source docker-entrypoint-initdb.d/init_sonarqube_db.sh
## (If you encounter logs error)
## docker-compose run --user=root --rm sonarqube chown sonarqube:sonarqube /opt/sonarqube/logs
SONARQUBE_HOSTNAME=sonar.example.com
SONARQUBE_PORT=9000
SONARQUBE_POSTGRES_INIT=true
SONARQUBE_POSTGRES_HOST=postgres
SONARQUBE_POSTGRES_DB=sonar
SONARQUBE_POSTGRES_USER=sonar
SONARQUBE_POSTGRES_PASSWORD=sonarPass

76
.env.laradock.example Normal file
View File

@ -0,0 +1,76 @@
###################################
# Blessing Skin Server v4 #
# Configuration #
###################################
# 本文件中各个配置项的详细说明
# 请访问 http://t.cn/E9G4DTY 查看中文教程
# Be sure to disable debug at production environment!
APP_DEBUG = false
APP_ENV = production
# Database Configuration
DB_CONNECTION = mysql
DB_HOST = mariadb
DB_PORT = 3306
DB_DATABASE = default
DB_USERNAME = default
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, PHP_PASSWORD_HASH
# - MD5, SALTED2MD5
# - SHA256, SALTED2SHA256
# - SHA512, SALTED2SHA512
#
# New sites are *highly* recommended to use BCRYPT.
#
PWD_METHOD = BCRYPT
# 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 = 2c5ca184f017a9a1ffbd198ef69b0c0e
# App Key should be setted to any random, *32 character* string,
# otherwise all the encrypted strings will not be safe.
#
# You can run [php artisan key:generate] to generate a new key.
#
APP_KEY = base64:gkb/zouNF6UOSfnr/o+izVMS57WQS3+62YqZBuDyBhU=
# 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 = null
# Change below lines only if you know what they mean!
CACHE_DRIVER = file
SESSION_DRIVER = file
QUEUE_DRIVER = sync
REDIS_HOST = 127.0.0.1
REDIS_PASSWORD = null
REDIS_PORT = 6379
PLUGINS_DIR = null
PLUGINS_URL = null

View File

@ -1,105 +1,39 @@
APP_DEBUG=false
APP_ENV=testing
###################################
# Blessing Skin Server v4 #
# 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_DEBUG = false
APP_ENV = testing
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 = BCRYPT
SALT = c67709dd8b7b733aca0d570681fe96cf
APP_KEY = base64:eVX/xzF5NhpGB2luswliFx9XSBsbbAP21wOi68X/P34=
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
PLUGINS_DIR=plugins
PLUGINS_URL=
REDIS_HOST = 127.0.0.1
REDIS_PASSWORD = null
REDIS_PORT = 6379
TEXTURES_DIR=
PLUGINS_DIR = plugins
PLUGINS_URL = null
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-----"
JWT_SECRET = 1tdM3gXarxYI4KlAHMBo238iC2tEb4I3EtBlZTQQXvInXIt7V2ix7hJ1KTvxCKZW

View File

@ -2,8 +2,4 @@ public/
vendor/
coverage/
plugins/
node_modules/
*.d.ts
resources/assets/tests/__mocks__/
resources/assets/tests/ts-shims/
resources/assets/tests/*.ts
laradock/

View File

@ -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

View File

@ -16,20 +16,17 @@
- [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`,否则无法编译前端资源
然后执行 `cp .env.example .env`,并`.env` 中配置好您的环境信息。
### 进行开发
@ -37,18 +34,7 @@ yarn
`.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 信息。
`APP_ENV` 为其它值时,您需要事先执行 `yarn build`。此命令将构建并压缩前端资源。通常用于生产环境。
### 测试

28
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,28 @@
<!-- 在提交一个新 issue 前,请先阅读以下内容: -->
<!-- Before opening a new issue, please make sure to READ the articles below: -->
<!-- * FAQ 常见问题: https://git.io/fjRtn -->
<!-- * 报告问题的正确姿势https://git.io/fjRtc -->
<!-- 把下面模板中的占位文字删除,并按照你的情况认真填写,谢谢 -->
<!-- Please remove the placeholders and fill in the template according to your situation. -->
## The Problem 问题描述
## Environment 运行环境
- Blessing Skin 版本 (Version of Blessing Skin):
- 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. -->
<!-- 详细描述你出错前的操作步骤 -->

View File

@ -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

View File

@ -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

View File

@ -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: 请在那里报告问题。

View File

@ -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

View File

@ -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

View File

@ -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 }}

11
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
.env
.sass-cache
coverage
@ -5,7 +6,6 @@ coverage
.cache/
.cache-loader/
vendor/*
storage/textures
storage/textures/*
storage/update_cache/*
node_modules/*
@ -17,13 +17,6 @@ _ide_helper.php
junit.xml
storage/*.db
storage/*.sqlite
storage/insane-profile-cache
.vscode
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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "laradock"]
path = laradock
url = https://github.com/Laradock/laradock.git

View File

@ -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

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn pretty-quick --staged

View File

@ -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);

26
.travis.yml Normal file
View File

@ -0,0 +1,26 @@
language: php
php: 7.3
install: true
script: true
before_deploy:
- nvm install 10
- node -v
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.15.2
- export PATH="$HOME/.yarn/bin:$PATH"
- RELEASE_TAG=$TRAVIS_TAG ./scripts/release.sh
deploy:
provider: releases
api_key: $GITHUB_TOKEN
file: blessing-skin-server-$TRAVIS_TAG.zip
name: $TRAVIS_TAG
skip_cleanup: true
on:
tags: true
after_deploy:
- curl -s -o github-release.tar.bz2 -L https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2
- tar -xf github-release.tar.bz2
- ./bin/linux/amd64/github-release -v edit -u bs-community -r blessing-skin-server -t $TRAVIS_TAG -d "$(cat resources/misc/changelogs/en/$TRAVIS_TAG.md && echo -e '\n---\n' && cat resources/misc/changelogs/zh_CN/$TRAVIS_TAG.md)"

View File

@ -1,7 +0,0 @@
{
"recommendations": [
"editorconfig.editorconfig",
"bmewburn.vscode-intelephense-client",
"esbenp.prettier-vscode"
]
}

34
.vscode/launch.json vendored
View File

@ -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}/"
}
]
}
]
}

View File

@ -1,3 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -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"]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016-present The Blessing Skin Team
Copyright (c) 2016-present The Blessing Skin Community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -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/),转载请注明。

239
README.md
View File

@ -1,90 +1,209 @@
- [简体中文](./README-zh.md)
- **English**
- <b>简体中文</b>
- [English](./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://circleci.com/gh/bs-community/blessing-skin-server"><img src="https://flat.badgen.net/circleci/github/bs-community/blessing-skin-server" alt="Circle CI Status"></a>
<a href="https://codecov.io/gh/bs-community/blessing-skin-server/branch"><img src="https://flat.badgen.net/codecov/c/github/bs-community/blessing-skin-server" alt="Codecov" /></a>
<a href="https://github.com/bs-community/blessing-skin-server/releases"><img src="https://flat.badgen.net/github/release/bs-community/blessing-skin-server" alt="Latest Stable Version"></a>
<img src="https://flat.badgen.net/badge/PHP/7.1.8+/orange" alt="PHP 7.1.8+">
<img src="https://flat.badgen.net/github/license/bs-community/blessing-skin-server" alt="License">
</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 项目,这意味着您可以自由地在您的服务器上部署它。
## 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
- 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)
- 完整实现了一个皮肤站该有的功能
- 支持单用户多个角色
- 通过皮肤库来分享您的皮肤和披风!
- 易于使用
- 可视化的用户、角色、材质管理页面
- 详细的站点配置页面
- 多处 UI/UX 优化只为更好的用户体验
- 安全
- 支持多种安全密码 Hash 算法
- 注册可要求 Email 验证
- 防止恶意请求的积分系统
- 强大的可扩展性
- 多种多样的插件
- 支持与 Authme/Discuz 等程序的用户数据对接(插件)
- 支持自定义 Yggdrasil API 外置登录系统(插件)
## Requirements
## 环境要求
Blessing Skin has only a few system requirements. In most cases, these PHP extensions are already enabled.
Blessing Skin 对您的服务器有一定的要求。_在大多数情况下下列所需的 PHP 扩展已经开启。_
- 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
- 一台支持 URL 重写的主机Nginx、Apache 或 IIS
- **PHP >= 7.1.8** [(服务器不支持?)](https://github.com/bs-community/blessing-skin-server/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E)
- 安装并启用如下 PHP 扩展:
- OpenSSL
- PDO
- Mbstring
- Tokenizer
- GD
- XML
- Ctype
- JSON
- fileinfo
## Quick Install
## 快速使用
Please read [Installation Guide](https://blessing.netlify.app/en/setup.html).
请参阅 [Wiki - 快速安装向导](https://github.com/bs-community/blessing-skin-server/wiki/%E5%BF%AB%E9%80%9F%E5%AE%89%E8%A3%85%E5%90%91%E5%AF%BC)。
## Plugin System
![screenshot](https://img.blessing.studio/images/2017/07/29/2017-06-16_15.54.16.png)
Blessing Skin provides an elegant and powerful plugin system, and you can attach plenty of functions and customization to your site via installing plugins.
## 插件系统
## Build From Source
Blessing Skin 提供了强大的插件系统,您可以通过添加多种多样的插件来为您的皮肤站添加功能。
Please refer to [Manual Build](https://blessing.netlify.app/build.html).
详情请参阅 [Wiki - 插件系统介绍](https://github.com/bs-community/blessing-skin-server/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BB%8B%E7%BB%8D)。
## Internationalization
## 支持并赞助 Blessing Skin
Blessing Skin supports multiple languages, while currently supporting English, Simplified Chinese and Spanish.
如果您觉得这个软件对您很有帮助,欢迎通过赞助来支持开发!
If you are willing to contribute your translation, welcome to join [our Crowdin project](https://crowdin.com/project/blessing-skin).
目前可在 [爱发电](https://afdian.net/@blessing-skin) 上赞助。
## Report Bugs
### Sponsors
Read [FAQ](https://blessing.netlify.app/faq.html) and double check if your situation doesn't suit any case mentioned there before reporting.
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/@hyx5020">
<img src="https://pic.afdiancdn.com/user/ff73629a6fa811e9abe252540025c377/avatar/b6c5f51467a2036d80d8103840aea9d4_w3264_h1836_s635.jpeg?imageView2/1/w/120/h/120">
<br>
hyx5020
</a>
</td>
<td align=center>
<a href="https://afdian.net/u/68d07bf851fc11e98e5652540025c377">
<img src="https://pic.afdiancdn.com/user/68d07bf851fc11e98e5652540025c377/avatar/59b21c3d053a595086d4b6cf88877bfa_w640_h640_s57.jpg?imageView2/1/w/120/h/120">
<br>
dz_paji
</a>
</td>
<td align=center>
<a href="https://afdian.net/@ExDragine">
<img src="https://pic.afdiancdn.com/user/ad213afe31b311e991c252540025c377/avatar/33d21c924f446a41073caa5d88be69b8_w200_h200_s36.jpg?imageView2/1/w/120/h/120">
<br>
ExDragine
</a>
</td>
<td align=center>
<a href="https://afdian.net/@akkariin">
<img src="https://pic.afdiancdn.com/user/f3f747da859011e98ebe52540025c377/avatar/14752883229fa9f346884dec196a4b8a_w256_h256_s35.jpg?imageView2/1/w/120/h/120">
<br>
Akkariin
</a>
</td>
</tr>
</tbody>
</table>
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.
### Backers
## Related Links
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/u/4d9a803ea8a211e9ba9052540025c377">
<img src="https://pic.afdiancdn.com/default/avatar/default-avatar@2x.png?imageView2/1/w/75/h/75">
<br>
爱发电用户_4ft3
</a>
</td>
<td align=center>
<a href="https://afdian.net/u/a08078a051fc11e9ab4c52540025c377">
<img src="https://pic.afdiancdn.com/user/a08078a051fc11e9ab4c52540025c377/avatar/9e25e37208832a1a41893ad1bd30a398_w628_h626_s39.jpg?imageView2/1/w/75/h/75">
<br>
pppwaw
</a>
</td>
<td align=center>
<a href="https://afdian.net/@tnqzh123">
<img src="https://pic.afdiancdn.com/user/97a0416ca47211e8849452540025c377/avatar/d2f6d8d489cb952ff29740e715b067c0_w768_h768_s211.jpg?imageView2/1/w/75/h/75">
<br>
Little_Qiu
</a>
</td>
<td align=center>
<a href="https://afdian.net/@hempflower">
<img src="https://pic.afdiancdn.com/user/0f396eb2a37c11e8b93452540025c377/avatar/bee35eb0f5cd2a506eb34c6e13de1154_w160_h160_s0.jpg?imageView2/1/w/75/h/75">
<br>
麻花
</a>
</td>
<td align=center>
<a href="https://afdian.net/@mgcraft">
<img src="https://pic.afdiancdn.com/user/de46a20a56f111e981a452540025c377/avatar/ab13b606230af1b5f5879538d9e37c43_w640_h640_s22.jpeg?imageView2/1/w/75/h/75">
<br>
Mangocraft
</a>
</td>
<td align=center>
<a href="https://afdian.net/@acilicraft">
<img src="https://pic.afdiancdn.com/user/63d4adac633311e98d9d52540025c377/avatar/50c279016873b7907ce7b901de1f560c_w577_h525_s248.jpg?imageView2/1/w/75/h/75">
<br>
Andy_Chuck
</a>
</td>
</tr>
</tbody>
</table>
- [User Manual](https://blessing.netlify.app/en/)
- [Plugins Development Documentation](https://bs-plugin.netlify.app/)
## 自行构建
## Copyright & License
如果你想为此项目作贡献,或者抢先尝试未发布的新功能,你应该先用 GitHub 上的代码部署。
**不推荐不熟悉 shell 操作以及不想折腾的用户使用。**
请先确保您安装好以下工具:
- [Git](https://git-scm.org)
- [Node.js](https://nodejs.org)
- [Yarn](https://yarnpkg.com)
- [Composer](https://getcomposer.org)
从 GitHub 上 clone 源码并安装依赖:
```bash
git clone https://github.com/bs-community/blessing-skin-server.git
cd blessing-skin-server
composer install
yarn
```
构建前端代码!
```bash
yarn build
```
接下来请参考「快速安装向导」进行后续安装。
## 国际化i18n
Blessing Skin 可支持多种语言,当前支持英语(`en`)和简体中文(`zh_CN`)。
当然,您也可以添加您自己的语言。请参阅 [Wiki - 添加其它语言 [i18n]](https://github.com/bs-community/blessing-skin-server/wiki/%E6%B7%BB%E5%8A%A0%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80-%5Bi18n%5D)
如果您愿意将您的翻译贡献出来,欢迎参与 [我们的 Crowdin 项目](https://crowdin.com/project/bs-i18n)。
## 问题报告
请参阅 [Wiki - 报告问题的正确姿势](https://github.com/bs-community/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)。
## 版权
MIT License
Copyright (c) 2016-present The Blessing Skin Team
Copyright (c) 2016-present The Blessing Skin Community
程序原作者为 [@printempw](https://blessing.studio/),转载请注明。

232
README_EN.md Normal file
View File

@ -0,0 +1,232 @@
- [简体中文](./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://circleci.com/gh/bs-community/blessing-skin-server"><img src="https://flat.badgen.net/circleci/github/bs-community/blessing-skin-server" alt="Circle CI Status"></a>
<a href="https://codecov.io/gh/bs-community/blessing-skin-server/branch"><img src="https://flat.badgen.net/codecov/c/github/bs-community/blessing-skin-server" alt="Codecov" /></a>
<a href="https://github.com/bs-community/blessing-skin-server/releases"><img src="https://flat.badgen.net/github/release/bs-community/blessing-skin-server" alt="Latest Stable Version"></a>
<img src="https://flat.badgen.net/badge/PHP/7.1.8+/orange" alt="PHP 7.1.8+">
<img src="https://flat.badgen.net/github/license/bs-community/blessing-skin-server" alt="License">
</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!
## 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
- Score system for preventing evil requests
- Incredibly extensible
- Plenty of plugins available
- Integration with Authme/CrazyLogin/Discuz (available as plugin)
- Support custom Yggdrasil API authentication (available as plugin)
## Requirements
Blessing Skin has only a few system requirements. _In most cases, these PHP extensions are already enabled._
- Web server with URL rewriting enabled
- **PHP >= 7.1.8** (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
- XML PHP Extension
- Ctype PHP Extension
- JSON PHP Extension
- fileinfo PHP Extension
## Quick Install
1. Download our [latest release](https://github.com/bs-community/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/bs-community/blessing-skin-server/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BB%8B%E7%BB%8D).
## Supporting Blessing Skin
Welcome to sponsoring Blessing Skin if this software is useful for you!
Currently you can sponsor us via [爱发电](https://afdian.net/@blessing-skin).
### Sponsors
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/@hyx5020">
<img src="https://pic.afdiancdn.com/user/ff73629a6fa811e9abe252540025c377/avatar/b6c5f51467a2036d80d8103840aea9d4_w3264_h1836_s635.jpeg?imageView2/1/w/120/h/120">
<br>
hyx5020
</a>
</td>
<td align=center>
<a href="https://afdian.net/u/68d07bf851fc11e98e5652540025c377">
<img src="https://pic.afdiancdn.com/user/68d07bf851fc11e98e5652540025c377/avatar/59b21c3d053a595086d4b6cf88877bfa_w640_h640_s57.jpg?imageView2/1/w/120/h/120">
<br>
dz_paji
</a>
</td>
<td align=center>
<a href="https://afdian.net/@ExDragine">
<img src="https://pic.afdiancdn.com/user/ad213afe31b311e991c252540025c377/avatar/33d21c924f446a41073caa5d88be69b8_w200_h200_s36.jpg?imageView2/1/w/120/h/120">
<br>
ExDragine
</a>
</td>
<td align=center>
<a href="https://afdian.net/@akkariin">
<img src="https://pic.afdiancdn.com/user/f3f747da859011e98ebe52540025c377/avatar/14752883229fa9f346884dec196a4b8a_w256_h256_s35.jpg?imageView2/1/w/120/h/120">
<br>
Akkariin
</a>
</td>
</tr>
</tbody>
</table>
### Backers
<table>
<tbody>
<tr>
<td align=center>
<a href="https://afdian.net/u/4d9a803ea8a211e9ba9052540025c377">
<img src="https://pic.afdiancdn.com/default/avatar/default-avatar@2x.png?imageView2/1/w/75/h/75">
<br>
爱发电用户_4ft3
</a>
</td>
<td align=center>
<a href="https://afdian.net/u/a08078a051fc11e9ab4c52540025c377">
<img src="https://pic.afdiancdn.com/user/a08078a051fc11e9ab4c52540025c377/avatar/9e25e37208832a1a41893ad1bd30a398_w628_h626_s39.jpg?imageView2/1/w/75/h/75">
<br>
pppwaw
</a>
</td>
<td align=center>
<a href="https://afdian.net/@tnqzh123">
<img src="https://pic.afdiancdn.com/user/97a0416ca47211e8849452540025c377/avatar/d2f6d8d489cb952ff29740e715b067c0_w768_h768_s211.jpg?imageView2/1/w/75/h/75">
<br>
Little_Qiu
</a>
</td>
<td align=center>
<a href="https://afdian.net/@hempflower">
<img src="https://pic.afdiancdn.com/user/0f396eb2a37c11e8b93452540025c377/avatar/bee35eb0f5cd2a506eb34c6e13de1154_w160_h160_s0.jpg?imageView2/1/w/75/h/75">
<br>
麻花
</a>
</td>
<td align=center>
<a href="https://afdian.net/@mgcraft">
<img src="https://pic.afdiancdn.com/user/de46a20a56f111e981a452540025c377/avatar/ab13b606230af1b5f5879538d9e37c43_w640_h640_s22.jpeg?imageView2/1/w/75/h/75">
<br>
Mangocraft
</a>
</td>
<td align=center>
<a href="https://afdian.net/@acilicraft">
<img src="https://pic.afdiancdn.com/user/63d4adac633311e98d9d52540025c377/avatar/50c279016873b7907ce7b901de1f560c_w577_h525_s248.jpg?imageView2/1/w/75/h/75">
<br>
Andy_Chuck
</a>
</td>
</tr>
</tbody>
</table>
## Developer Install
If you'd like make some contribution on the project, please deploy it from GitHub first.
**You'd better have some experience on shell operations to continue.**
Please make sure you have installed the tools below:
- [Git](https://git-scm.org)
- [Node.js](https://nodejs.org)
- [Yarn](https://yarnpkg.com)
- [Composer](https://getcomposer.org)
Clone the code from GitHub and install dependencies:
```bash
git clone https://github.com/bs-community/blessing-skin-server.git
cd blessing-skin-server
composer install
yarn
```
Build the things!
```bash
yarn 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/bs-community/blessing-skin-server/wiki/%E5%A6%82%E4%BD%95%E9%85%8D%E7%BD%AE%E7%9A%AE%E8%82%A4-Mod).
![screenshot](https://img.blessing.studio/images/2017/07/29/2017-06-16_15.54.16.png)
## Internationalization
Blessing Skin supports multiple languages, while currently supporting English (`en`) and Simplified Chinese (`zh_CN`).
Of course, you can add your own language. Please check [Wiki - Add other language [i18n]](https://github.com/bs-community/blessing-skin-server/wiki/%E6%B7%BB%E5%8A%A0%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80-%5Bi18n%5D) (Simplified Chinese only).
If you are willing to contribute your translation, welcome to join [our Crowdin project](https://crowdin.com/project/bs-i18n).
## Report Bugs
Read [Wiki - FAQ](https://github.com/bs-community/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/bs-community/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
MIT License
Copyright (c) 2016-present The Blessing Skin Community

View File

@ -3,35 +3,37 @@
namespace App\Console\Commands;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class BsInstallCommand extends Command
class ExecuteInstallation extends Command
{
protected $signature = 'bs:install {email} {password} {nickname}';
protected $description = 'Execute installation and create a super administrator.';
public function handle(Filesystem $filesystem)
public function handle()
{
if ($filesystem->exists(storage_path('install.lock'))) {
if (\App\Http\Controllers\SetupController::checkTablesExist()) {
$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
if (config('app.env') != 'testing') {
$this->call('key:random');
}
$this->call('salt:random');
$this->call('migrate', ['--force' => true]);
$this->call('jwt:secret', ['--no-interaction' => true]);
$this->call('passport:keys', ['--no-interaction' => true]);
option(['site_url' => url('/')]);
$siteUrl = url('/');
if (ends_with($siteUrl, '/index.php')) {
$siteUrl = substr($siteUrl, 0, -10);
}
option(['site_url' => $siteUrl]);
$admin = new User();
$admin = new User;
$admin->email = $this->argument('email');
$admin->nickname = $this->argument('nickname');
$admin->score = option('user_initial_score');
@ -39,13 +41,11 @@ class BsInstallCommand extends Command
$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->register_at = get_datetime_string();
$admin->last_sign_at = get_datetime_string(time() - 86400);
$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.');
}

View 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 handle()
{
$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
));
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands;
use DB;
use Schema;
use App\Models\User;
use Illuminate\Console\Command;
class MigrateCloset extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bs:migrate-v4:closet';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate the closet for v4';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! Schema::hasTable('closets')) {
$this->info('Nothing to do.');
return;
}
$this->info('We will migrate all closets data. Please wait...');
$rows = DB::table('closets')->select('*')->get();
$bar = $this->output->createProgressBar($rows->count());
$rows->map(function ($row) use ($bar) {
$closet = User::find($row->uid)->closet();
collect(json_decode($row->textures, true))->each(function ($item) use ($closet) {
$closet->attach($item['tid'], ['item_name' => $item['name']]);
});
$bar->advance();
});
Schema::drop('closets');
$bar->finish();
$this->info("\nCongrats! Everything are done.");
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Console\Commands;
use Schema;
use App\Models\Player;
use Illuminate\Console\Command;
use Illuminate\Database\Schema\Blueprint;
class MigratePlayersTable extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bs:migrate-v4:players-table';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate the players table for v4';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! Schema::hasColumn('players', 'tid_steve')) {
$this->info('No need to update.');
return;
}
$players = Player::where('tid_skin', -1)->get();
$count = $players->count();
if ($count == 0) {
$this->dropColumn();
$this->info('No need to update.');
return;
}
$this->info('We are going to update your `players` table. Please wait...');
$bar = $this->output->createProgressBar($count);
$players->each(function ($player) use ($bar) {
$player->tid_skin = $player->preference == 'default'
? $player->tid_steve
: $player->tid_alex;
$player->save();
$bar->advance();
});
$this->dropColumn();
$bar->finish();
$this->info("\nCongratulations! We've updated $count rows.");
}
private function dropColumn()
{
Schema::table('players', function (Blueprint $table) {
$table->dropColumn(['tid_steve', 'tid_alex', 'preference']);
});
}
}

View File

@ -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.');
}
}

View File

@ -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'));
}
}
}

View File

@ -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'));
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use App\Models\Texture;
use Illuminate\Console\Command;
class RegressLikesField extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bs:migrate-v4:likes';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Apply fixes for `likes` field of `textures` table.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('We are going to update your `textures` table. Please wait...');
$textures = Texture::all();
$bar = $this->output->createProgressBar($textures->count());
$textures->each(function ($texture) use ($bar) {
$texture->likes = $texture->likers->count();
$texture->save();
$bar->advance();
});
$bar->finish();
$this->info("\nCongratulations! Table was updated successfully.");
}
}

View File

@ -6,10 +6,25 @@ 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';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$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));
}
}

View File

@ -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']);
}
},
]);
}
}

View File

@ -6,13 +6,18 @@ 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\KeyRandomCommand::class,
Commands\SaltRandomCommand::class,
Commands\UpdateCommand::class,
Commands\MigratePlayersTable::class,
Commands\MigrateCloset::class,
Commands\ExecuteInstallation::class,
Commands\RegressLikesField::class,
];
}

View 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;
}
}

View File

@ -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

View File

@ -6,6 +6,12 @@ class ConfigureExploreMenu extends Event
{
public $menu;
/**
* Create a new event instance.
*
* @param array $menu
* @return void
*/
public function __construct(array &$menu)
{
// Pass array by reference

View File

@ -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;

View File

@ -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;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Events;
use App\Models\User;
class EncryptUserPassword extends Event
{
public $user;
public $raw;
/**
* Create a new event instance.
*
* @param string $raw The raw password before encrypted.
* @param User $user
* @return void
*/
public function __construct($raw, User $user)
{
$this->raw = $raw;
$this->user = $user;
}
}

View File

@ -4,4 +4,5 @@ namespace App\Events;
abstract class Event
{
//
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -2,12 +2,9 @@
namespace App\Exceptions;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Laravel\Passport\Exceptions\MissingScopeException;
use Throwable;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
@ -18,46 +15,29 @@ class Handler extends ExceptionHandler
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Validation\ValidationException::class,
\Illuminate\Session\TokenMismatchException::class,
ModelNotFoundException::class,
PrettyPageException::class,
];
public function render($request, Throwable $exception)
{
if ($exception instanceof ModelNotFoundException) {
$model = $exception->getModel();
if (Str::endsWith($model, 'Texture')) {
$exception = new ModelNotFoundException(trans('skinlib.non-existent'));
}
} elseif ($exception instanceof MissingScopeException) {
return json($exception->getMessage(), 403);
}
return parent::render($request, $exception);
}
protected function convertExceptionToArray(Throwable $e)
protected function convertExceptionToArray(Exception $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) {
return Arr::only($trace, ['file', 'line']);
})
->filter(function ($trace) {
return Arr::has($trace, 'file');
})
->map(function ($trace) {
$trace['file'] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $trace['file']);
return $trace;
})
->filter(function ($trace) {
// @codeCoverageIgnoreStart
$isFromPlugins = !app()->runningUnitTests()
&& Str::contains($trace['file'], resolve('plugins')->getPluginsDirs()->all());
// @codeCoverageIgnoreEnd
return Str::startsWith($trace['file'], 'app') || $isFromPlugins;
return \Illuminate\Support\Str::startsWith($trace['file'], 'app');
})
->values(),
];

View File

@ -2,67 +2,55 @@
namespace App\Http\Controllers;
use Cache;
use Option;
use Notification;
use Carbon\Carbon;
use App\Models\User;
use App\Notifications;
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 Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redis;
class AdminController extends Controller
{
public function index(Filter $filter)
{
$grid = [
'layout' => [
['md-6', 'md-6'],
],
'widgets' => [
[
[
'admin.widgets.dashboard.usage',
'admin.widgets.dashboard.notification',
],
['admin.widgets.dashboard.chart'],
],
],
];
$grid = $filter->apply('grid:admin.index', $grid);
return view('admin.index', [
'grid' => $grid,
'sum' => [
'users' => User::count(),
'players' => Player::count(),
'textures' => Texture::count(),
'storage' => Texture::select('size')->sum('size'),
],
]);
}
public function chartData()
{
$xAxis = Collection::times(31, fn ($i) => Carbon::today()->subDays(31 - $i)->isoFormat('l'));
$today = Carbon::today()->timestamp;
$oneMonthAgo = Carbon::today()->subMonth();
$xAxis = Collection::times(31, function ($number) use ($today) {
$time = Carbon::createFromTimestamp($today - (31 - $number) * 86400);
$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;
return $time->format('m-d');
});
$oneMonthAgo = Carbon::createFromTimestamp($today - 30 * 86400);
$grouping = function ($field) {
return function ($item) use ($field) {
return substr($item->$field, 5, 5);
};
};
$mapping = function ($item) {
return count($item);
};
$aligning = function ($data) {
return function ($day) use ($data) {
return $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()
@ -82,68 +70,512 @@ class AdminController extends Controller
];
}
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,
public function sendNotification(Request $request)
{
$data = $this->validate($request, [
'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',
]);
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()) : '';
$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 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->checkbox('hide_pray_for_kyoto_animation')->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。')
->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') {
$color = $this->validate($request, ['color' => 'required'])['color'];
option(['color_scheme' => $color]);
}
$grid = [
'layout' => [
['md-6', 'md-6'],
],
'widgets' => [
[
['admin.widgets.status.info'],
['admin.widgets.status.plugins'],
],
],
];
$grid = $filter->apply('grid:admin.status', $grid);
return view('admin.customize', ['forms' => compact('homepage', 'customJsCss')]);
}
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'),
],
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 (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')
->text('player_name_length_min')
->addon('~')
->text('player_name_length_max')
->addon(trans('options.general.player_name_length.suffix'));
$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->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();
$form->textarea('comment_script')->rows(6)->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->checkbox('return_204_when_notfound')->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();
$redis = Option::form('redis', 'Redis', function ($form) {
$form->checkbox('enable_redis')->label()->description();
});
if (option('enable_redis')) {
try {
Redis::ping();
$redis->addMessage(trans('options.redis.connect.success'), 'success');
} catch (\Exception $e) {
$redis->addMessage(
trans('options.redis.connect.failed', ['msg' => $e->getMessage()]),
'danger'
);
}
}
$redis->handle();
$cache = Option::form('cache', OptionForm::AUTO_DETECT, function ($form) {
$form->checkbox('enable_avatar_cache')->label();
$form->checkbox('enable_preview_cache')->label();
$form->checkbox('enable_json_cache', 'JSON Profile')->label();
$form->checkbox('enable_notfound_cache', '404')->label();
})
->type('warning')
->addButton([
'text' => trans('options.cache.clear'),
'type' => 'a',
'class' => 'pull-right',
'style' => 'warning',
'href' => '?clear-cache',
])
->with('plugins', $enabledPlugins);
->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', 'redis', 'cache'));
}
public function getUserData(Request $request)
{
$isSingleUser = $request->has('uid');
if ($isSingleUser) {
$users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at', 'verified'])
->where('uid', intval($request->input('uid')))
->get();
} else {
$search = $request->input('search', '');
$sortField = $request->input('sortField', 'uid');
$sortType = $request->input('sortType', 'asc');
$page = $request->input('page', 1);
$perPage = $request->input('perPage', 10);
$users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at', 'verified'])
->where('uid', 'like', '%'.$search.'%')
->orWhere('email', 'like', '%'.$search.'%')
->orWhere('nickname', 'like', '%'.$search.'%')
->orWhere('score', 'like', '%'.$search.'%')
->orderBy($sortField, $sortType)
->offset(($page - 1) * $perPage)
->limit($perPage)
->get();
}
$users->transform(function ($user) {
$user->operations = auth()->user()->permission;
$user->players_count = $user->players->count();
return $user;
});
return [
'totalRecords' => $isSingleUser ? 1 : User::count(),
'data' => $users,
];
}
public function getPlayerData(Request $request)
{
$isSpecifiedUser = $request->has('uid');
if ($isSpecifiedUser) {
$players = Player::select(['pid', 'uid', 'name', 'tid_skin', 'tid_cape', 'last_modified'])
->where('uid', intval($request->input('uid')))
->get();
} else {
$search = $request->input('search', '');
$sortField = $request->input('sortField', 'pid');
$sortType = $request->input('sortType', 'asc');
$page = $request->input('page', 1);
$perPage = $request->input('perPage', 10);
$players = Player::select(['pid', 'uid', 'name', 'tid_skin', 'tid_cape', 'last_modified'])
->where('pid', 'like', '%'.$search.'%')
->orWhere('uid', 'like', '%'.$search.'%')
->orWhere('name', 'like', '%'.$search.'%')
->orderBy($sortField, $sortType)
->offset(($page - 1) * $perPage)
->limit($perPage)
->get();
}
return [
'totalRecords' => $isSpecifiedUser ? 1 : Player::count(),
'data' => $players,
];
}
public function userAjaxHandler(Request $request)
{
$action = $request->input('action');
$user = User::find($request->uid);
$currentUser = Auth::user();
if (! $user) {
return json(trans('admin.users.operations.non-existent'), 1);
}
if ($user->uid !== $currentUser->uid && $user->permission >= $currentUser->permission) {
return json(trans('admin.users.operations.no-permission'), 1);
}
if ($action == 'email') {
$this->validate($request, [
'email' => 'required|email',
]);
if (User::where('email', $request->email)->count() != 0) {
return json(trans('admin.users.operations.email.existed', ['email' => $request->input('email')]), 1);
}
$user->email = $request->input('email');
$user->save();
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->nickname = $request->input('nickname');
$user->save();
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->changePassword($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 == 'permission') {
$user->permission = $this->validate($request, [
'permission' => 'required|in:-1,0,1',
])['permission'];
$user->save();
return json([
'code' => 0,
'message' => trans('admin.users.operations.permission'),
]);
} elseif ($action == 'delete') {
$user->delete();
return json(trans('admin.users.operations.delete.success'), 0);
} else {
return json(trans('admin.users.operations.invalid'), 1);
}
}
public function playerAjaxHandler(Request $request)
{
$action = $request->input('action');
$currentUser = Auth::user();
$player = Player::find($request->input('pid'));
if (! $player) {
return json(trans('general.unexistent-player'), 1);
}
$owner = $player->user;
if (
$owner && $owner->uid !== $currentUser->uid &&
$owner->permission >= $currentUser->permission
) {
return json(trans('admin.players.no-permission'), 1);
}
if ($action == 'texture') {
$this->validate($request, [
'type' => 'required',
'tid' => 'required|integer',
]);
if (! Texture::find($request->tid) && $request->tid != 0) {
return json(trans('admin.players.textures.non-existent', ['tid' => $request->tid]), 1);
}
$field = 'tid_'.$request->type;
$player->$field = $request->tid;
$player->save();
return json(trans('admin.players.textures.success', ['player' => $player->name]), 0);
} elseif ($action == 'owner') {
$this->validate($request, [
'uid' => 'required|integer',
]);
$user = User::find($request->uid);
if (! $user) {
return json(trans('admin.users.operations.non-existent'), 1);
}
$player->uid = $request->input('uid');
$player->save();
return json(trans('admin.players.owner.success', ['player' => $player->name, 'user' => $user->nickname]), 0);
} elseif ($action == 'delete') {
$player->delete();
return json(trans('admin.players.delete.success'), 0);
} elseif ($action == 'name') {
$name = $this->validate($request, [
'name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max'),
])['name'];
$player->name = $name;
$player->save();
if (option('single_player', false) && $owner) {
$owner->nickname = $name;
$owner->save();
}
return json(trans('admin.players.name.success', ['player' => $player->name]), 0);
} else {
return json(trans('admin.users.operations.invalid'), 1);
}
}
}

View File

@ -2,237 +2,166 @@
namespace App\Http\Controllers;
use URL;
use Mail;
use View;
use Cache;
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 App\Rules\Captcha;
use App\Mail\ForgotPassword;
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;
class AuthController extends Controller
{
public function login(Filter $filter)
public function handleLogin(Request $request, Captcha $captcha)
{
$whip = new Whip();
$ip = $whip->getValidIpAddress();
$ip = $filter->apply('client_ip', $ip);
$rows = [
'auth.rows.login.notice',
'auth.rows.login.message',
'auth.rows.login.form',
'auth.rows.login.registration-link',
];
$rows = $filter->apply('auth_page_rows:login', $rows);
return view('auth.login', [
'rows' => $rows,
'extra' => [
'tooManyFails' => cache(sha1('login_fails_'.$ip)) > 3,
'recaptcha' => option('recaptcha_sitekey'),
'invisible' => (bool) option('recaptcha_invisible'),
],
]);
}
public function handleLogin(
Request $request,
Rules\Captcha $captcha,
Dispatcher $dispatcher,
Filter $filter,
) {
$data = $request->validate([
$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';
$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;
$user = $player ? $player->user : null;
}
// 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]]);
$this->validate($request, ['captcha' => ['required', $captcha]]);
}
if (!$user) {
if (! $user) {
return json(trans('auth.validation.user'), 2);
} else {
if ($user->verifyPassword($request->input('password'))) {
Session::forget('login_fails');
Auth::login($user, $request->input('keep'));
event(new Events\UserLoggedIn($user));
Cache::forget($loginFailsCacheKey);
return json(trans('auth.login.success'), 0, [
'redirectTo' => $request->session()->pull('last_requested_path', url('/user')),
]);
} else {
// Increase the counter
Cache::put($loginFailsCacheKey, ++$loginFails, 3600);
return json(trans('auth.validation.password'), 1, [
'login_fails' => $loginFails,
]);
}
}
}
$dispatcher->dispatch('auth.login.ready', [$user]);
public function logout()
{
if (Auth::check()) {
Auth::logout();
if ($user->verifyPassword($request->input('password'))) {
Session::forget('login_fails');
Cache::forget($loginFailsCacheKey);
return json(trans('auth.logout.success'), 0);
} else {
return json(trans('auth.logout.fail'), 1);
}
}
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')),
public function register()
{
if (option('user_can_register')) {
return view('auth.register', [
'extra' => [
'player' => (bool) option('register_with_player_name'),
'recaptcha' => option('recaptcha_sitekey'),
'invisible' => (bool) option('recaptcha_invisible'),
],
]);
} else {
$loginFails++;
Cache::put($loginFailsCacheKey, $loginFails, 3600);
$dispatcher->dispatch('auth.login.failed', [$user, $loginFails]);
return json(trans('auth.validation.password'), 1, [
'login_fails' => $loginFails,
]);
throw new PrettyPageException(trans('auth.register.close'), 7);
}
}
public function logout(Dispatcher $dispatcher)
public function handleRegister(Request $request, Captcha $captcha)
{
$user = Auth::user();
$dispatcher->dispatch('auth.logout.before', [$user]);
Auth::logout();
$dispatcher->dispatch('auth.logout.after', [$user]);
return json(trans('auth.logout.success'), 0);
}
public function register(Filter $filter)
{
$rows = [
'auth.rows.register.notice',
'auth.rows.register.form',
];
$rows = $filter->apply('auth_page_rows:register', $rows);
return view('auth.register', [
'site_name' => option_localized('site_name'),
'rows' => $rows,
'extra' => [
'player' => (bool) option('register_with_player_name'),
'recaptcha' => option('recaptcha_sitekey'),
'invisible' => (bool) option('recaptcha_invisible'),
],
]);
}
public function handleRegister(
Request $request,
Rules\Captcha $captcha,
Dispatcher $dispatcher,
Filter $filter,
) {
$can = $filter->apply('can_register', null);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
if (! option('user_can_register')) {
return json(trans('auth.register.close'), 7);
}
$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',
['player_name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max')] :
['nickname' => 'required|no_special_chars|max:255'];
$data = $this->validate($request, array_merge([
'email' => 'required|email|unique:users',
'password' => 'required|min:8|max:32',
'captcha' => ['required', $captcha],
'captcha' => ['required', $captcha],
], $rule));
$playerName = $request->input('player_name');
$dispatcher->dispatch('auth.registration.attempt', [$data]);
if (option('register_with_player_name')) {
event(new Events\CheckPlayerExists($request->get('player_name')));
if (
option('register_with_player_name')
&& Player::where('name', $playerName)->count() > 0
) {
return json(trans('user.player.add.repeated'), 1);
if (Player::where('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 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);
// 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);
}
$dispatcher->dispatch('auth.registration.ready', [$data]);
$user = new User();
$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->password = User::getEncryptedPwdFromEvent($data['password'], $user)
?: app('cipher')->hash($data['password'], config('secure.salt'));
$user->ip = get_client_ip();
$user->permission = User::NORMAL;
$user->register_at = Carbon::now();
$user->last_sign_at = Carbon::now()->subDay();
$user->register_at = get_datetime_string();
$user->last_sign_at = get_datetime_string(time() - 86400);
$user->save();
$dispatcher->dispatch('auth.registration.completed', [$user]);
event(new Events\UserRegistered($user));
if (option('register_with_player_name')) {
$dispatcher->dispatch('player.adding', [$playerName, $user]);
$player = new Player();
$player = new Player;
$player->uid = $user->uid;
$player->name = $playerName;
$player->name = $request->get('player_name');
$player->tid_skin = 0;
$player->save();
$dispatcher->dispatch('player.added', [$player, $user]);
event(new Events\PlayerWasAdded($player));
}
$dispatcher->dispatch('auth.login.ready', [$user]);
Auth::login($user);
$dispatcher->dispatch('auth.login.succeeded', [$user]);
return json(trans('auth.register.success'), 0);
}
public function forgot()
{
if (config('mail.default') != '') {
if (config('mail.driver') != '') {
return view('auth.forgot', [
'extra' => [
'recaptcha' => option('recaptcha_sitekey'),
@ -244,99 +173,65 @@ class AuthController extends Controller
}
}
public function handleForgot(
Request $request,
Rules\Captcha $captcha,
Dispatcher $dispatcher,
Filter $filter,
) {
$data = $request->validate([
'email' => 'required|email',
public function handleForgot(Request $request, Captcha $captcha)
{
$this->validate($request, [
'captcha' => ['required', $captcha],
]);
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);
}
$user = User::where('email', $email)->first();
if (!$user) {
$user = User::where('email', $request->email)->first();
if (! $user) {
return json(trans('auth.forgot.unregistered'), 1);
}
$dispatcher->dispatch('auth.forgot.ready', [$user]);
$url = URL::temporarySignedRoute('auth.reset', now()->addHour(), ['uid' => $user->uid]);
$url = URL::temporarySignedRoute(
'auth.reset',
Carbon::now()->addHour(),
['uid' => $user->uid],
false
);
try {
Mail::to($email)->send(new ForgotPassword(url($url)));
Mail::to($request->input('email'))->send(new ForgotPassword($url));
} catch (\Exception $e) {
report($e);
$dispatcher->dispatch('auth.forgot.failed', [$user, $url]);
return json(trans('auth.forgot.failed', ['msg' => $e->getMessage()]), 2);
}
$dispatcher->dispatch('auth.forgot.sent', [$user, $url]);
Cache::put($lastMailCacheKey, time(), 3600);
return json(trans('auth.forgot.success'), 0);
}
public function reset(Request $request, $uid)
public function reset($uid)
{
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
return view('auth.reset')->with('user', User::find($uid));
}
public function handleReset(Dispatcher $dispatcher, Request $request, $uid)
public function handleReset($uid, Request $request)
{
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
['password' => $password] = $request->validate([
$validated = $this->validate($request, [
'password' => 'required|min:8|max:32',
]);
$user = User::find($uid);
$dispatcher->dispatch('auth.reset.before', [$user, $password]);
$user->changePassword($password);
$dispatcher->dispatch('auth.reset.after', [$user, $password]);
User::find($uid)->changePassword($validated['password']);
return json(trans('auth.reset.success'), 0);
}
public function captcha(\Gregwar\Captcha\CaptchaBuilder $builder)
{
$builder->build(100, 34);
session(['captcha' => $builder->getPhrase()]);
return response($builder->output(), 200, [
'Content-Type' => 'image/jpeg',
'Cache-Control' => 'no-store',
]);
}
public function fillEmail(Request $request)
{
$email = $request->validate(['email' => 'required|email|unique:users'])['email'];
$email = $this->validate($request, ['email' => 'required|email|unique:users'])['email'];
$user = $request->user();
$user->email = $email;
$user->save();
@ -344,30 +239,43 @@ class AuthController extends Controller
return redirect('/user');
}
public function verify(Request $request)
public function verify($uid)
{
if (!option('require_verification')) {
if (! option('require_verification')) {
throw new PrettyPageException(trans('user.verification.disabled'), 1);
}
abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid'));
$user = User::find($uid);
return view('auth.verify');
}
public function handleVerify(Request $request, User $user)
{
abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid'));
['email' => $email] = $request->validate(['email' => 'required|email']);
if ($user->email !== $email) {
return back()->with('errorMessage', trans('auth.verify.not-matched'));
if (! $user || $user->verified) {
throw new PrettyPageException(trans('auth.verify.invalid'), 1);
}
$user->verified = true;
$user->save();
return redirect()->route('user.home');
return view('auth.verify');
}
public function jwtLogin(Request $request)
{
$token = Auth::guard('jwt')->attempt([
'email' => $request->email,
'password' => $request->password,
]) ?: '';
return json(compact('token'));
}
public function jwtLogout()
{
Auth::guard('jwt')->logout();
return response('', 204);
}
public function jwtRefresh()
{
return json(['token' => Auth::guard('jwt')->refresh()]);
}
}

View File

@ -2,39 +2,20 @@
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\Texture;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ClosetController extends Controller
{
public function index(Filter $filter)
public function index()
{
$grid = [
'layout' => [
['md-8', 'md-4'],
],
'widgets' => [
[
[
'user.widgets.email-verification',
'user.widgets.closet.list',
],
['shared.previewer'],
],
],
];
$grid = $filter->apply('grid:user.closet', $grid);
return view('user.closet')
->with('grid', $grid)
->with('extra', [
'unverified' => option('require_verification') && !auth()->user()->verified,
'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',
@ -46,63 +27,64 @@ class ClosetController extends Controller
public function getClosetData(Request $request)
{
$category = $request->input('category', 'skin');
/** @var User */
$page = abs($request->input('page', 1));
$perPage = (int) $request->input('perPage', 6);
$q = $request->input('q', null);
$perPage = $perPage > 0 ? $perPage : 6;
$user = auth()->user();
$closet = $user->closet();
return $user
->closet()
->when(
$category === 'cape',
fn (Builder $query) => $query->where('type', 'cape'),
fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
)
->when(
$request->input('q'),
fn (Builder $query, $search) => $query->like('item_name', $search)
)
->orderBy('texture_tid', 'DESC')
->paginate((int) $request->input('perPage', 6));
}
public function allIds()
{
/** @var User */
$user = auth()->user();
return $user->closet()->pluck('texture_tid');
}
public function add(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
) {
['tid' => $tid, 'name' => $name] = $request->validate([
'tid' => 'required|integer',
'name' => 'required',
]);
/** @var User */
$user = Auth::user();
$name = $filter->apply('add_closet_item_name', $name, [$tid]);
$dispatcher->dispatch('closet.adding', [$tid, $name, $user]);
$can = $filter->apply('can_add_closet_item', true, [$tid, $name]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
if ($category == 'cape') {
$closet = $closet->where('type', 'cape');
} else {
$closet = $closet->where(function ($query) {
return $query->where('type', 'steve')->orWhere('type', 'alex');
});
}
if ($q) {
$closet = $closet->where('item_name', 'like', "%$q%");
}
$total = $closet->count();
$closet->offset(($page - 1) * $perPage)->limit($perPage);
$totalPages = ceil($total / $perPage);
$items = $closet->get()->map(function ($t) {
$t->name = $t->pivot->item_name;
return $t;
});
return json('', 0, [
'category' => $category,
'items' => $items,
'total_pages' => $totalPages,
]);
}
public function add(Request $request)
{
$this->validate($request, [
'tid' => 'required|integer',
'name' => 'required|no_special_chars',
]);
$user = Auth::user();
if ($user->score < option('score_per_closet_item')) {
return json(trans('user.closet.add.lack-score'), 1);
return json(trans('user.closet.add.lack-score'), 7);
}
$tid = $request->tid;
$texture = Texture::find($tid);
if (!$texture) {
if (! $texture) {
return json(trans('user.closet.add.not-found'), 1);
}
if (!$texture->public && ($texture->uploader != $user->uid && !$user->isAdmin())) {
if (! $texture->public && $texture->uploader != $user->uid) {
return json(trans('skinlib.show.private'), 1);
}
@ -111,14 +93,11 @@ class ClosetController extends Controller
}
$user->closet()->attach($tid, ['item_name' => $request->name]);
$user->score -= option('score_per_closet_item');
$user->save();
$user->setScore(option('score_per_closet_item'), 'minus');
$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);
@ -128,67 +107,38 @@ class ClosetController extends Controller
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 */
public function rename(Request $request, $tid)
{
$this->validate($request, ['name' => 'required|no_special_chars']);
$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)) {
if ($user->closet()->where('tid', $request->tid)->count() == 0) {
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($request->tid, ['item_name' => $request->name]);
$user->closet()->updateExistingPivot($tid, ['item_name' => $name]);
$dispatcher->dispatch('closet.renamed', [$item, $previousName, $user]);
return json(trans('user.closet.rename.success', ['name' => $name]), 0);
return json(trans('user.closet.rename.success', ['name' => $request->name]), 0);
}
public function remove(Dispatcher $dispatcher, Filter $filter, $tid)
public function remove($tid)
{
/** @var User */
$user = auth()->user();
$dispatcher->dispatch('closet.removing', [$tid, $user]);
$item = $user->closet()->find($tid);
if (empty($item)) {
if ($user->closet()->where('tid', $tid)->count() == 0) {
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();
$user->setScore(option('score_per_closet_item'), 'plus');
}
$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();

View File

@ -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'));
}
}

View File

@ -3,11 +3,10 @@
namespace App\Http\Controllers;
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;
}

View File

@ -2,40 +2,21 @@
namespace App\Http\Controllers;
use Illuminate\Support\Arr;
class HomeController extends Controller
{
public function index()
{
return view('home')
->with('user', auth()->user())
->with('site_description', option_localized('site_description'))
->with('transparent_navbar', (bool) option('transparent_navbar', false))
->with('fixed_bg', option('fixed_bg'))
->with('hide_intro', option('hide_intro'))
return view('index')->with('user', auth()->user())
->with('transparent_navbar', option('transparent_navbar', false))
->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,
'copyright' => bs_copyright(),
'site_name' => option('site_name'),
]);
}

View File

@ -2,36 +2,55 @@
namespace App\Http\Controllers;
use App\Services\Plugin;
use App\Services\PluginManager;
use App\Services\Unzip;
use Composer\CaBundle\CaBundle;
use Composer\Semver\Comparator;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Request;
use App\Services\PluginManager;
use Composer\Semver\Comparator;
use App\Services\PackageManager;
class MarketController extends Controller
{
public function marketData(PluginManager $manager)
/**
* Guzzle HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $guzzle;
/**
* 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;
}
public function marketData()
{
$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', []);
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;
@ -40,61 +59,78 @@ class MarketController extends Controller
return $plugins;
}
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']);
if (!$metadata) {
return $plugin && Comparator::greaterThan($item['version'], $plugin->version);
});
return json([
'available' => $pluginsHaveUpdate->isNotEmpty(),
'plugins' => $pluginsHaveUpdate->values()->all(),
]);
}
public function download(Request $request, PluginManager $manager, PackageManager $package)
{
$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);
$url = $metadata['dist']['url'];
$filename = Arr::last(explode('/', $url));
$pluginsDir = $manager->getPluginsDir();
$path = storage_path("packages/$name".'_'.$metadata['version'].'.zip');
return json(trans('admin.plugins.market.unresolved'), 1, compact('reason'));
try {
$package->download($url, $path, $metadata['dist']['shasum'])->extract($pluginsDir);
} catch (Exception $e) {
return json($e->getMessage(), 1);
}
$path = tempnam(sys_get_temp_dir(), $name);
$response = Http::withOptions([
'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get($metadata['dist']['url']);
if ($response->ok()) {
$unzip->extract($path, $manager->getPluginsDirs()->first());
return json(trans('admin.plugins.market.install-success'), 0);
} else {
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
}
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));
if ($response->ok()) {
return $response->json()['packages'];
} else {
throw new Exception(trans('admin.plugins.market.connection-error', ['error' => $response->status()]));
protected function getAllAvailablePlugins()
{
$registryVersion = 1;
if (app()->runningUnitTests() || ! $this->registryCache) {
$registries = collect(explode(',', config('plugins.registry')));
$this->registryCache = $registries->map(function ($registry) use ($registryVersion) {
try {
$pluginsJson = $this->guzzle->request(
'GET',
trim($registry),
['verify' => resource_path('misc/ca-bundle.crt')]
)->getBody();
} catch (Exception $e) {
throw new Exception(trans('admin.plugins.market.connection-error', [
'error' => htmlentities($e->getMessage()),
]));
}
})
->flatten(1);
return $plugins;
$registryData = json_decode($pluginsJson, true);
$received = Arr::get($registryData, 'version');
abort_if(
is_int($received) && $received != $registryVersion,
500,
"Only version $registryVersion of market registry is accepted."
);
return Arr::get($registryData, 'packages', []);
})->flatten(1);
}
return $this->registryCache;
}
}

View File

@ -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(),
];
}
}

View File

@ -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'));
}
}

View File

@ -2,259 +2,204 @@
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 App\Events\PlayerWasAdded;
use App\Events\PlayerWasDeleted;
use App\Events\CheckPlayerExists;
use App\Events\PlayerWillBeAdded;
use App\Events\PlayerWillBeDeleted;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use App\Http\Middleware\CheckPlayerExist;
use App\Http\Middleware\CheckPlayerOwner;
class PlayerController extends Controller
{
public function __construct()
{
$this->middleware(function (Request $request, $next) {
/** @var Player */
$player = $request->route('player');
if ($player->user->isNot($request->user())) {
return json(trans('admin.players.no-permission'), 1)
->setStatusCode(403);
}
return $next($request);
}, [
$this->middleware([CheckPlayerExist::class, CheckPlayerOwner::class], [
'only' => ['delete', 'rename', 'setTexture', 'clearTexture'],
]);
}
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'),
]);
}
public function list()
public function listAll()
{
return Auth::user()->players;
return json(
'',
0,
Auth::user()
->players()
->select('pid', 'name', 'tid_skin', 'tid_cape')
->get()
->toArray()
);
}
public function add(Request $request, Dispatcher $dispatcher, Filter $filter)
public function add(Request $request)
{
/** @var User */
$user = Auth::user();
$name = $request->validate([
'name' => [
'required',
new Rules\PlayerName(),
'min:'.option('player_name_length_min'),
'max:'.option('player_name_length_max'),
'unique:players',
],
])['name'];
$name = $filter->apply('new_player_name', $name);
$dispatcher->dispatch('player.add.attempt', [$name, $user]);
$can = $filter->apply('can_add_player', true, [$name]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
if (option('single_player', false)) {
return json(trans('user.player.add.single'), 1);
}
if ($user->score < (int) option('score_per_player')) {
$name = $this->validate($request, [
'name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max'),
])['name'];
event(new CheckPlayerExists($name));
if (! Player::where('name', $name)->get()->isEmpty()) {
return json(trans('user.player.add.repeated'), 6);
}
if ($user->score < Option::get('score_per_player')) {
return json(trans('user.player.add.lack-score'), 7);
}
$dispatcher->dispatch('player.adding', [$name, $user]);
event(new PlayerWillBeAdded($name));
$player = new Player();
$player = new Player;
$player->uid = $user->uid;
$player->name = $name;
$player->tid_skin = 0;
$player->tid_cape = 0;
$player->save();
$user->score -= (int) option('score_per_player');
$user->save();
$dispatcher->dispatch('player.added', [$player, $user]);
event(new PlayerWasAdded($player));
$user->setScore(option('score_per_player'), 'minus');
return json(trans('user.player.add.success', ['name' => $name]), 0, $player->toArray());
}
public function delete(
Dispatcher $dispatcher,
Filter $filter,
Player $player,
) {
/** @var User */
$user = auth()->user();
public function delete($pid)
{
$player = Player::find($pid);
$playerName = $player->name;
$dispatcher->dispatch('player.delete.attempt', [$player, $user]);
$can = $filter->apply('can_delete_player', true, [$player]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
if (option('single_player', false)) {
return json(trans('user.player.delete.single'), 1);
}
$dispatcher->dispatch('player.deleting', [$player, $user]);
event(new PlayerWillBeDeleted($player));
$player->delete();
if (option('return_score')) {
$user->score += (int) option('score_per_player');
$user->save();
Auth::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),
],
public function rename(Request $request, $pid)
{
$newName = $this->validate($request, [
'name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max'),
])['name'];
$name = $filter->apply('new_player_name', $name);
$player = Player::find($pid);
$dispatcher->dispatch('player.renaming', [$player, $name]);
$can = $filter->apply('can_rename_player', true, [$player, $name]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
if (! Player::where('name', $newName)->get()->isEmpty()) {
return json(trans('user.player.rename.repeated'), 6);
}
$old = $player->replicate();
$player->name = $name;
$oldName = $player->name;
$player->name = $newName;
$player->save();
$dispatcher->dispatch('player.renamed', [$player, $old]);
if (option('single_player', false)) {
$user = auth()->user();
$user->nickname = $newName;
$user->save();
}
return json(
trans('user.player.rename.success', ['old' => $old->name, 'new' => $name]),
0,
$player->toArray()
);
return json(trans('user.player.rename.success', ['old' => $oldName, 'new' => $newName]), 0, $player->toArray());
}
public function setTexture(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
Player $player,
) {
/** @var User */
$user = auth()->user();
public function setTexture(Request $request, $pid)
{
$player = Player::find($pid);
foreach (['skin', 'cape'] as $type) {
$tid = $request->input($type);
$can = $filter->apply('can_set_texture', true, [$player, $type, $tid]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
if ($tid) {
if ($tid = $request->input($type)) {
$texture = Texture::find($tid);
if (empty($texture)) {
if (! $texture) {
return json(trans('skinlib.non-existent'), 1);
}
if ($user->closet()->where('texture_tid', $tid)->doesntExist()) {
return json(trans('user.closet.remove.non-existent'), 1);
}
$dispatcher->dispatch('player.texture.updating', [$player, $texture]);
$field = "tid_$type";
$player->$field = $tid;
$player->save();
$dispatcher->dispatch('player.texture.updated', [$player, $texture]);
}
}
return json(trans('user.player.set.success', ['name' => $player->name]), 0, $player->toArray());
}
public function clearTexture(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
Player $player,
) {
$types = $request->input('type', []);
foreach (['skin', 'cape'] as $type) {
$can = $filter->apply('can_clear_texture', true, [$player, $type]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
if ($request->has($type) || in_array($type, $types)) {
$dispatcher->dispatch('player.texture.resetting', [$player, $type]);
public function clearTexture(Request $request, $pid)
{
$player = Player::find($pid);
array_map(function ($type) use ($request, $player) {
if (
$request->has($type) ||
($request->has('type') && in_array($type, $request->input('type')))
) {
$field = "tid_$type";
$player->$field = 0;
$player->save();
$dispatcher->dispatch('player.texture.reset', [$player, $type]);
}
}
}, ['skin', 'cape']);
$player->save();
return json(trans('user.player.clear.success', ['name' => $player->name]), 0, $player->toArray());
}
public function bind(Request $request)
{
$name = $this->validate($request, [
'player' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max'),
])['player'];
$user = Auth::user();
event(new CheckPlayerExists($name));
$player = Player::where('name', $name)->first();
if (! $player) {
event(new PlayerWillBeAdded($name));
$player = new Player;
$player->uid = $user->uid;
$player->name = $name;
$player->tid_skin = 0;
$player->save();
event(new PlayerWasAdded($player));
} elseif ($player->uid != $user->uid) {
return json(trans('user.player.rename.repeated'), 1);
}
$user->players()->where('name', '<>', $name)->delete();
$user->nickname = $name;
$user->save();
return json(trans('user.player.bind.success'), 0);
}
}

View File

@ -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);
}
}

View File

@ -3,55 +3,25 @@
namespace App\Http\Controllers;
use App\Services\Plugin;
use App\Services\PluginManager;
use App\Services\Unzip;
use Composer\CaBundle\CaBundle;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use League\CommonMark\GithubFlavoredMarkdownConverter;
use App\Services\PluginManager;
class PluginController extends Controller
{
public function config(PluginManager $plugins, $name)
public function config($name)
{
$plugin = $plugins->get($name);
if ($plugin && $plugin->isEnabled()) {
if ($plugin->hasConfigClass()) {
return app()->call($plugin->getConfigClass().'@render');
} elseif ($plugin->hasConfigView()) {
return $plugin->getConfigView();
} else {
return abort(404, trans('admin.plugins.operations.no-config-notice'));
}
$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 +29,33 @@ 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'];
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(trans('admin.plugins.operations.unsatisfied.notice'), 1, compact('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 +69,31 @@ class PluginController extends Controller
public function getPluginData(PluginManager $plugins)
{
return $plugins->all()
->map(function (Plugin $plugin) {
return $plugins->getPlugins()
->map(function ($plugin) {
return [
'name' => $plugin->name,
'title' => trans($plugin->title),
'description' => trans($plugin->description ?? ''),
'title' => trans($plugin->title ?: 'EMPTY'),
'author' => $plugin->author,
'description' => trans($plugin->description ?: 'EMPTY'),
'version' => $plugin->version,
'url' => $plugin->url,
'enabled' => $plugin->isEnabled(),
'readme' => (bool) $plugin->getReadme(),
'config' => $plugin->hasConfig(),
'icon' => array_merge(
['fa' => 'plug', 'faType' => 'fas', 'bg' => 'navy'],
$plugin->getManifestAttr('enchants.icon', [])
),
'config' => $plugin->hasConfigView(),
'dependencies' => $this->getPluginDependencies($plugin),
];
})
->values();
}
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),
];
}
}

View File

@ -2,37 +2,23 @@
namespace App\Http\Controllers;
use App\Models\User;
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)
public function submit(Request $request)
{
$data = $request->validate([
$data = $this->validate($request, [
'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) {
if (Report::where('reporter', $reporter->uid)->where('tid', $data['tid'])->count() > 0) {
return json(trans('skinlib.report.duplicate'), 1);
}
@ -43,54 +29,71 @@ class ReportController extends Controller
$reporter->score += $score;
$reporter->save();
$report = new Report();
$report->tid = $tid;
$report->uploader = Texture::find($tid)->uploader;
$report = new Report;
$report->tid = $data['tid'];
$report->uploader = Texture::find($data['tid'])->uploader;
$report->reporter = $reporter->uid;
$report->reason = $reason;
$report->reason = $data['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())
return Report::where('reporter', auth()->id())
->orderBy('report_at', 'desc')
->paginate(10);
return view('user.report', ['reports' => $reports]);
->get();
}
public function manage(Request $request)
{
$q = $request->input('q');
$search = $request->input('search', '');
$sortField = $request->input('sortField', 'report_at');
$sortType = $request->input('sortType', 'desc');
$page = $request->input('page', 1);
$perPage = $request->input('perPage', 10);
return Report::usingSearchString($q)
->with(['texture', 'textureUploader', 'informer'])
->paginate(9);
$reports = Report::where('tid', 'like', '%'.$search.'%')
->orWhere('reporter', 'like', '%'.$search.'%')
->orWhere('reason', 'like', '%'.$search.'%')
->orderBy($sortField, $sortType)
->offset(($page - 1) * $perPage)
->limit($perPage)
->get()
->makeHidden(['informer'])
->map(function ($report) {
$uploader = User::find($report->uploader);
if ($uploader) {
$report->uploaderName = $uploader->nickname;
}
if ($report->informer) {
$report->reporterName = $report->informer->nickname;
}
return $report;
});
return [
'totalRecords' => Report::count(),
'data' => $reports,
];
}
public function review(
Report $report,
Request $request,
Dispatcher $dispatcher,
) {
$data = $request->validate([
public function review(Request $request)
{
$data = $this->validate($request, [
'id' => 'required|exists:reports',
'action' => ['required', Rule::in(['delete', 'ban', 'reject'])],
]);
$action = $data['action'];
$report = Report::find($data['id']);
$dispatcher->dispatch('report.reviewing', [$report, $action]);
if ($action == 'reject') {
if ($data['action'] == 'reject') {
if (
$report->informer
&& ($score = option('reporter_score_modification', 0)) > 0
&& $report->status == Report::PENDING
$report->informer &&
($score = option('reporter_score_modification', 0)) > 0 &&
$report->status == Report::PENDING
) {
$report->informer->score -= $score;
$report->informer->save();
@ -98,35 +101,25 @@ class ReportController extends Controller
$report->status = Report::REJECTED;
$report->save();
$dispatcher->dispatch('report.rejected', [$report]);
return json(trans('general.op-success'), 0, ['status' => Report::REJECTED]);
}
switch ($action) {
switch ($data['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]);
if ($report->texture) {
$report->texture->delete();
} 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) {
if (! $uploader) {
return json(trans('admin.users.operations.non-existent'), 1);
}
if (auth()->user()->permission <= $uploader->permission) {
@ -134,7 +127,6 @@ class ReportController extends Controller
}
$uploader->permission = User::BANNED;
$uploader->save();
$dispatcher->dispatch('user.banned', [$uploader]);
break;
}
@ -143,26 +135,17 @@ class ReportController extends Controller
$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
) {
static function returnScore($report) {
if ($report->status == Report::PENDING && ($score = option('reporter_score_modification', 0)) < 0) {
$report->informer->score -= $score;
$report->informer->save();
}
}
public static function giveAward($report)
{
if ($report->status == Report::PENDING && $report->informer) {
static function giveAward($report) {
if ($report->status == Report::PENDING) {
$report->informer->score += option('reporter_reward_score', 0);
$report->informer->save();
}

View File

@ -2,40 +2,42 @@
namespace App\Http\Controllers;
use App\Exceptions\PrettyPageException;
use DB;
use Log;
use File;
use Option;
use Schema;
use Artisan;
use Storage;
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 App\Exceptions\PrettyPageException;
class SetupController extends Controller
{
public function database(
Request $request,
Filesystem $filesystem,
Connection $connection,
DatabaseManager $manager,
) {
public function welcome()
{
// @codeCoverageIgnoreStart
if (! File::exists(base_path('.env'))) {
File::copy(base_path('.env.example'), base_path('.env'));
}
// @codeCoverageIgnoreEnd
return view('setup.wizard.welcome');
}
public function database(Request $request)
{
if ($request->isMethod('get')) {
try {
$connection->getPdo();
DB::getPdo();
return redirect('setup/info');
// @codeCoverageIgnoreStart
} 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'),
]);
return view('setup.wizard.database');
// @codeCoverageIgnoreEnd
}
}
@ -50,108 +52,239 @@ class SetupController extends Controller
]);
try {
$manager->connection('temp')->getPdo();
DB::connection('temp')->getPdo();
} catch (\Exception $e) {
$msg = $e->getMessage();
$type = Arr::get([
'mysql' => 'MySQL/MariaDB',
'sqlite' => 'SQLite',
'pgsql' => 'PostgreSQL',
], $request->input('type'), '');
$msg = iconv('gbk', 'utf-8', $e->getMessage());
$type = humanize_db_type($request->input('type'));
throw new PrettyPageException(trans('setup.database.connection-error', compact('msg', 'type')), $e->getCode());
throw new PrettyPageException(
trans('setup.database.connection-error', compact('msg', 'type')),
$e->getCode()
);
}
$content = $filesystem->get(base_path('.env'));
$content = File::get(base_path('.env'));
$content = preg_replace(
'/DB_CONNECTION.+/',
'DB_CONNECTION='.$request->input('type', ''),
'DB_CONNECTION = '.$request->input('type'),
$content
);
$content = preg_replace(
'/DB_HOST.+/',
'DB_HOST='.$request->input('host', ''),
'DB_HOST = '.$request->input('host'),
$content
);
$content = preg_replace(
'/DB_PORT.+/',
'DB_PORT='.$request->input('port', ''),
'DB_PORT = '.$request->input('port'),
$content
);
$content = preg_replace(
'/DB_DATABASE.+/',
'DB_DATABASE='.$request->input('db', ''),
'DB_DATABASE = '.$request->input('db'),
$content
);
$content = preg_replace(
'/DB_USERNAME.+/',
'DB_USERNAME='.$request->input('username', ''),
'DB_USERNAME = '.$request->input('username'),
$content
);
$content = preg_replace(
'/DB_PASSWORD.+/',
'DB_PASSWORD='.$request->input('password', ''),
'DB_PASSWORD = '.$request->input('password'),
$content
);
$content = preg_replace(
'/DB_PREFIX.+/',
'DB_PREFIX='.$request->input('prefix', ''),
'DB_PREFIX = '.$request->input('prefix'),
$content
);
$filesystem->put(base_path('.env'), $content);
File::put(base_path('.env'), $content);
return redirect('setup/info');
}
public function finish(Request $request, Filesystem $filesystem, Artisan $artisan)
public function info()
{
$data = $request->validate([
'email' => 'required|email',
'nickname' => 'required',
'password' => 'required|min:8|max:32|confirmed',
$existingTables = static::checkTablesExist([], true);
// Not installed completely
if (count($existingTables) > 0) {
Log::info('Remaining tables detected, exit setup wizard 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)
{
$data = $this->validate($request, [
'email' => 'required|email',
'nickname' => 'required|no_special_chars|max:255',
'password' => 'required|min:8|max:32|confirmed',
'site_name' => 'required',
]);
$artisan->call('passport:keys', ['--no-interaction' => true]);
if ($request->has('generate_random')) {
Artisan::call('key:random');
Artisan::call('salt:random');
}
Artisan::call('jwt:secret', ['--no-interaction' => true]);
Artisan::call('passport:keys', ['--no-interaction' => true]);
// Create tables
$artisan->call('migrate', [
Artisan::call('migrate', [
'--force' => true,
'--path' => [
'database/migrations',
'vendor/laravel/passport/database/migrations',
],
]);
'vendor/laravel/passport/database/migrations'
]
]);
Log::info('[SetupWizard] Tables migrated.');
Option::set('site_name', $request->input('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 = 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->password = User::getEncryptedPwdFromEvent($data['password'], $user)
?: app('cipher')->hash($data['password'], config('secure.salt'));
$user->ip = get_client_ip();
$user->permission = User::SUPER_ADMIN;
$user->register_at = Carbon::now();
$user->last_sign_at = Carbon::now()->subDay();
$user->register_at = get_datetime_string();
$user->last_sign_at = get_datetime_string(time() - 86400);
$user->verified = true;
$user->save();
$filesystem->put(storage_path('install.lock'), '');
$this->createDirectories();
return view('setup.wizard.finish');
return view('setup.wizard.finish')->with([
'email' => $request->input('email'),
'password' => $request->input('password'),
]);
}
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;
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;
}
$tips = require database_path('update_scripts')."/$filename";
$updateScriptExist = true;
}
}
closedir($resource);
foreach (config('options') as $key => $value) {
if (! Option::has($key)) {
Option::set($key, $value);
}
}
Option::set('version', config('app.version'));
Artisan::call('view:clear');
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',
'user_closet',
'players',
'textures',
'options',
'reports',
];
foreach ($tables as $tableName) {
if (Schema::hasTable($tableName)) {
$existingTables[] = $tableName;
}
}
if (count($existingTables) == count($tables)) {
return $returnExistingTables ? $existingTables : true;
} else {
return $returnExistingTables ? $existingTables : false;
}
}
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();
}
}

View File

@ -2,174 +2,169 @@
namespace App\Http\Controllers;
use App\Models\Texture;
use View;
use Option;
use Session;
use Storage;
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\Player;
use App\Models\Texture;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Intervention\Image\Facades\Image;
use League\CommonMark\GithubFlavoredMarkdownConverter;
class SkinlibController extends Controller
{
public function __construct()
/**
* 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 index()
{
$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']);
return view('skinlib.index', ['user' => Auth::user()]);
}
public function library(Request $request)
/**
* 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));
// 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', '');
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 (! $user) {
// Show public textures only to anonymous visitors
$query = $query->where('public', true);
} else {
// Show private textures when show uploaded textures of current user
if ($uploader != $user->uid && ! $user->isAdmin()) {
$query = $query->where(function ($innerQuery) use ($user) {
$innerQuery->where('public', true)->orWhere('uploader', '=', $user->uid);
});
}
}
$totalPages = ceil($query->count() / $itemsPerPage);
$sort = $request->input('sort', 'time');
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
$query = $query->orderBy($sortBy, 'desc');
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);
$textures = $query->skip(($currentPage - 1) * $itemsPerPage)->take($itemsPerPage)->get();
if ($user) {
$closet = $user->closet()->get();
foreach ($textures as $item) {
$item->liked = $closet->contains('tid', $item->tid);
}
}
return json('', 0, [
'items' => $textures,
'current_uid' => $user ? $user->uid : 0,
'total_pages' => $totalPages,
]);
}
public function show(Filter $filter, Texture $texture)
public function show($tid)
{
/** @var User */
$texture = Texture::find($tid);
$user = Auth::user();
/** @var FilesystemAdapter */
$disk = Storage::disk('textures');
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'];
if (! $texture->public) {
if (! Auth::check() || ($user->uid != $texture->uploader && ! $user->isAdmin())) {
abort(option('status_code_for_private'), trans('skinlib.show.private'));
}
$badges = $filter->apply('user_badges', $badges, [$uploader]);
}
$grid = [
'layout' => [
['md-8', 'md-4'],
],
'widgets' => [
[
['shared.previewer'],
['skinlib.widgets.show.side'],
],
],
];
$grid = $filter->apply('grid:skinlib.show', $grid);
return view('skinlib.show')
->with('texture', $texture)
->with('grid', $grid)
->with('with_out_filter', true)
->with('user', $user)
->with('extra', [
'download' => (bool) option('allow_downloading_texture'),
'download' => 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'),
'nickname' => ($up = User::find($texture->uploader)) ? $up->nickname : null,
'report' => intval(option('reporter_score_modification', 0)),
'badges' => $badges,
]);
}
public function info(Texture $texture)
public function info($tid)
{
return $texture;
if ($t = Texture::find($tid)) {
return json('', 0, $t->toArray());
} else {
return abort(404);
}
}
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('user', Auth::user())
->with('extra', [
'rule' => ($regexp = option('texture_name_regexp'))
? trans('skinlib.upload.name-rule-regexp', compact('regexp'))
@ -178,284 +173,247 @@ class SkinlibController extends Controller
'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(),
]);
'scorePublic' => intval(option('score_per_storage')),
'scorePrivate' => intval(option('private_score_per_storage')),
'award' => intval(option('score_award_per_texture')),
'contentPolicy' => app('parsedown')->text(option_localized('content_policy')),
])
->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());
}
$data = $request->validate([
'name' => [
'required',
option('texture_name_regexp') ? 'regex:'.option('texture_name_regexp') : 'string',
],
'file' => 'required|mimes:png|max:'.option('max_upload_file_size'),
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
'public' => 'required|boolean',
]);
/** @var UploadedFile */
$file = $filter->apply('uploaded_texture_file', $file);
$name = $data['name'];
$name = $filter->apply('uploaded_texture_name', $name, [$file]);
$can = $filter->apply('can_upload_texture', true, [$file, $name]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$type = $data['type'];
$size = getimagesize($file);
$maxWidth = option('max_texture_width', 8192);
if ($size[0] > $maxWidth) {
$message = trans('skinlib.upload.too-wide', [
'width' => $size[0],
'maxWidth' => $maxWidth,
]);
return json($message, 1);
}
if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
$message = trans('skinlib.upload.invalid-size', [
'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'),
'width' => $size[0],
'height' => $size[1],
]);
return json($message, 1);
}
$ratio = $size[0] / $size[1];
if ($type == 'steve' || $type == 'alex') {
if ($ratio != 2 && $ratio != 1 || $type === 'alex' && $ratio === 2) {
$message = trans('skinlib.upload.invalid-size', [
'type' => trans('general.skin'),
'width' => $size[0],
'height' => $size[1],
]);
return json($message, 1);
}
} elseif ($type == 'cape') {
if ($ratio != 2) {
$message = trans('skinlib.upload.invalid-size', [
'type' => trans('general.cape'),
'width' => $size[0],
'height' => $size[1],
]);
return json($message, 1);
}
}
$image = Image::make($file);
$imagick = $image->getCore();
$imagick->setOption('png:compression-filter', '0');
$imagick->setOption('png:compression-level', '9');
$imagick->setOption('png:compression-strategy', '0');
$imagick->setOption('png:exclude-chunk', 'all');
$imagick->stripImage();
$sanitized = $image->encode('png')->getEncoded();
$hash = hash('sha256', $image->encoded);
$hash = $filter->apply('uploaded_texture_hash', $hash, [$image]);
/** @var User */
public function handleUpload(Request $request)
{
$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]);
if (($response = $this->checkUpload($request)) instanceof JsonResponse) {
return $response;
}
$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')
);
$file = $request->file('file');
$responses = event(new \App\Events\HashingFile($file));
if (isset($responses[0]) && is_string($responses[0])) {
return $responses[0]; // @codeCoverageIgnore
}
$t = new Texture();
$t->name = $request->input('name');
$t->type = $request->input('type');
$t->hash = hash_file('sha256', $file);
$t->size = ceil($request->file('file')->getSize() / 1024);
$t->public = $request->input('public') == 'true';
$t->uploader = $user->uid;
$cost = $t->size * ($t->public ? 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);
return json(trans('skinlib.upload.lack-score'), 7);
}
$dispatcher->dispatch('texture.uploading', [$image, $name, $hash]);
$results = Texture::where('hash', $t->hash)->get();
$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 (! $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) {
return json(trans('skinlib.upload.repeated'), 0, ['tid' => $result->tid]);
}
}
}
$user->score -= $cost;
$user->closet()->attach($texture->tid, ['item_name' => $name]);
$user->save();
if (! Storage::disk('textures')->exists($t->hash)) {
Storage::disk('textures')->put($t->hash, file_get_contents($request->file('file')));
}
$dispatcher->dispatch('texture.uploaded', [$texture, $image]);
$t->likes++;
$t->save();
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
'tid' => $texture->tid,
$user->setScore($cost, 'minus');
$user->closet()->attach($t->tid, ['item_name' => $t->name]);
return json(trans('skinlib.upload.success', ['name' => $request->input('name')]), 0, [
'tid' => $t->tid,
]);
}
public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter)
// @codeCoverageIgnore
public function delete(Request $request)
{
$can = $filter->apply('can_delete_texture', true, [$texture]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
$texture = Texture::find($request->tid);
$user = Auth::user();
if (! $texture) {
return json(trans('skinlib.non-existent'), 1);
}
$dispatcher->dispatch('texture.deleting', [$texture]);
if ($texture->uploader != $user->uid && ! $user->isAdmin()) {
return json(trans('skinlib.no-permission'), 1);
}
// check if file occupied
if (Texture::where('hash', $texture->hash)->count() === 1) {
if (Texture::where('hash', $texture->hash)->count() == 1) {
Storage::disk('textures')->delete($texture->hash);
}
$texture->delete();
$dispatcher->dispatch('texture.deleted', [$texture]);
return json(trans('skinlib.delete.success'), 0);
}
public function privacy(Texture $texture, Dispatcher $dispatcher, Filter $filter)
public function privacy(Request $request)
{
$can = $filter->apply('can_update_texture_privacy', true, [$texture]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
$t = Texture::find($request->input('tid'));
$user = Auth::user();
if (! $t) {
return json(trans('skinlib.non-existent'), 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)) {
if ($t->uploader != $user->uid && ! $user->isAdmin()) {
return json(trans('skinlib.no-permission'), 1);
}
$uploader = User::find($t->uploader);
$score_diff = $t->size * (option('private_score_per_storage') - option('score_per_storage')) * ($t->public ? -1 : 1);
if ($t->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]);
$type = $t->type == 'cape' ? 'cape' : 'skin';
Player::where("tid_$type", $t->tid)
->where('uid', '<>', session('uid'))
->update(["tid_$type" => 0]);
$t->likers()->get()->each(function ($user) use ($t) {
$user->closet()->detach($t->tid);
if (option('return_score')) {
$user->setScore(option('score_per_closet_item'), 'plus');
}
$t->likes--;
});
@$uploader->setScore($score_diff, 'plus');
$t->public = ! $t->public;
$t->save();
return json(
trans('skinlib.privacy.success', ['privacy' => (! $t->public ? trans('general.private') : trans('general.public'))]),
0
);
}
public function rename(Request $request)
{
$this->validate($request, [
'tid' => 'required|integer',
'new_name' => 'required|no_special_chars',
]);
$user = Auth::user();
$t = Texture::find($request->input('tid'));
if (! $t) {
return json(trans('skinlib.non-existent'), 1);
}
if ($t->uploader != $user->uid && ! $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)
{
$user = Auth::user();
$data = $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 != $user->uid && ! $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', ['name' => $duplicate->name]), 1);
}
$t->type = $request->input('model');
$t->save();
return json(trans('skinlib.model.success', ['model' => $data['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());
}
}
$dispatcher->dispatch('texture.privacy.updating', [$texture]);
$uploader->score += $score_diff;
$uploader->save();
$texture->public = !$texture->public;
$texture->save();
$dispatcher->dispatch('texture.privacy.updated', [$texture]);
$message = trans('skinlib.privacy.success', [
'privacy' => (
$texture->public
? trans('general.public')
: trans('general.private')),
$this->validate($request, [
'name' => [
'required',
option('texture_name_regexp') ? 'regex:'.option('texture_name_regexp') : 'no_special_chars',
],
'file' => 'required|max:'.option('max_upload_file_size'),
'public' => 'required',
]);
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);
$mime = $request->file('file')->getMimeType();
if ($mime != 'image/png' && $mime != 'image/x-png') {
return json(trans('skinlib.upload.type-error'), 1);
}
$dispatcher->dispatch('texture.name.updating', [$texture, $name]);
$type = $request->input('type');
$size = getimagesize($request->file('file'));
$ratio = $size[0] / $size[1];
$old = $texture->replicate();
$texture->name = $name;
$texture->save();
$dispatcher->dispatch('texture.name.updated', [$texture, $old]);
return json(trans('skinlib.rename.success', ['name' => $name]), 0);
}
public function type(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
Texture $texture,
) {
$data = $request->validate([
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
]);
$type = $data['type'];
$can = $filter->apply('can_update_texture_type', true, [$texture, $type]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
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);
}
$dispatcher->dispatch('texture.type.updating', [$texture, $type]);
$old = $texture->replicate();
$texture->type = $type;
$texture->save();
$dispatcher->dispatch('texture.type.updated', [$texture, $old]);
return json(trans('skinlib.model.success', ['model' => $type]), 0);
}
// @codeCoverageIgnore
}

View File

@ -2,179 +2,226 @@
namespace App\Http\Controllers;
use Event;
use Option;
use Storage;
use Response;
use Exception;
use Minecraft;
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 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',
]);
if ($api == 'csl') {
$content = $player->getJsonProfile(Player::CSL_API);
} elseif ($api == 'usm') {
$content = $player->getJsonProfile(Player::USM_API);
} else {
$content = $player->getJsonProfile(Option::get('api_type'));
}
return Response::jsonProfile($content, 200, [
'Last-Modified' => strtotime($player->last_modified),
]);
}
public function json($player)
public function jsonWithApi($api, $player_name)
{
$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);
return $this->json($player_name, $api);
}
public function previewByHash(Minecraft $minecraft, Request $request, $hash)
public function texture($hash, $headers = [], $message = '')
{
$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));
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) {
report($e);
}
return $response;
return abort(404, $message);
}
public function textureWithApi($api, $hash)
{
return $this->texture($hash);
}
public function skin($player_name)
{
return $this->getBinaryTextureFromPlayer($player_name, 'skin');
}
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 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]));
}
}
// @codeCoverageIgnore
public function avatarByTid($tid, $size = 128)
{
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
} else {
$png = Minecraft::generateAvatarFromSkin(Storage::disk('textures')->read($t->hash), $size);
return Response::png(png($png));
}
}
} catch (Exception $e) {
report($e);
}
}
return response()->file(storage_path('static_textures/avatar.png'));
}
public function avatarByTidWithSize($size, $tid)
{
return $this->avatarByTid($tid, $size);
}
public function avatar($base64_email, $size = 128)
{
$user = User::where('email', base64_decode($base64_email))->first();
if ($user) {
return $this->avatarByTid($user->avatar, $size);
}
return response()->file(storage_path('static_textures/avatar.png'));
}
public function avatarWithSize($size, $base64_email)
{
return $this->avatar($base64_email, $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 \Symfony\Component\HttpFoundation\Response) {
return $responses[0]; // @codeCoverageIgnore
} else {
$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);
}
return Response::png(png($png));
}
}
} catch (Exception $e) {
report($e);
}
}
// Show this if given texture is invalid.
return response()->file(storage_path('static_textures/broken.png'));
}
public function previewWithSize($size, $tid)
{
return $this->preview($tid, $size);
}
public function raw($tid)
{
abort_unless(option('allow_downloading_texture'), 403);
abort_unless(option('allow_downloading_texture'), 404);
$texture = Texture::findOrFail($tid);
return $this->texture($texture->hash);
return ($t = Texture::find($tid))
? $this->texture($t->hash)
: abort(404, trans('skinlib.non-existent'));
}
public function texture(string $hash)
public function avatarByPlayer($size, $name)
{
$disk = Storage::disk('textures');
abort_if($disk->missing($hash), 404);
$player = Player::where('name', $name)->first();
abort_unless($player, 404);
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
$hash = $player->getTexture('skin');
if (Storage::disk('textures')->has($hash)) {
$png = Minecraft::generateAvatarFromSkin(
Storage::disk('textures')->read($hash),
$size
);
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);
return Response::png(png($png));
}
$size = (int) $request->query('size', 100);
$mode = $request->has('3d') ? '3d' : '2d';
$usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
$format = $usePNG ? 'png' : 'webp';
return abort(404);
}
$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);
}
protected function getPlayerInstance($player_name)
{
$player = Player::where('name', $player_name)->first();
abort_if($player->isBanned(), 403, trans('general.player-banned'));
$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 $player;
}
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
// TODO: refactor
return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make($image)
->resize($size, $size)
->response($usePNG ? 'png' : 'webp', 100)
->setLastModified($lastModified);
}
);
return $response;
/**
* 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';
}
}

View File

@ -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);
}
}

View File

@ -2,92 +2,103 @@
namespace App\Http\Controllers;
use App\Services\Unzip;
use Cache;
use Composer\CaBundle\CaBundle;
use Composer\Semver\Comparator;
use Illuminate\Filesystem\Filesystem;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Request;
use Composer\Semver\Comparator;
use App\Services\PackageManager;
class UpdateController extends Controller
{
public const SPEC = 2;
protected $currentVersion;
protected $updateSource;
protected $guzzle;
protected $error;
protected $info = [];
public function __construct(\GuzzleHttp\Client $guzzle)
{
$this->updateSource = config('app.update_source');
$this->currentVersion = config('app.version');
$this->guzzle = $guzzle;
}
public function showUpdatePage()
{
$info = $this->getUpdateInfo();
$canUpdate = $this->canUpdate(Arr::get($info, 'info'));
$info = [
'latest' => Arr::get($this->getUpdateInfo(), 'latest'),
'current' => $this->currentVersion,
];
$error = $this->error;
$extra = ['canUpdate' => $this->canUpdate()];
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'],
]);
return view('admin.update', compact('info', 'error', 'extra'));
}
public function download(Unzip $unzip, Filesystem $filesystem)
public function checkUpdates()
{
$info = $this->getUpdateInfo();
if (!$info['ok'] || !$this->canUpdate($info['info'])['can']) {
return json(trans('admin.update.info.up-to-date'), 1);
return json(['available' => $this->canUpdate()]);
}
public function download(Request $request, PackageManager $package)
{
if (! $this->canUpdate()) {
return json([]);
}
$info = $info['info'];
$path = tempnam(sys_get_temp_dir(), 'bs');
$path = storage_path('packages/bs_'.$this->info['latest'].'.zip');
switch ($request->get('action')) {
case 'download':
try {
$package->download($this->info['url'], $path)->extract(base_path());
$response = Http::withOptions([
'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get($info['url']);
return json(trans('admin.update.complete'), 0);
} catch (Exception $e) {
report($e);
if ($response->ok()) {
$unzip->extract($path, base_path());
// Delete options cache. This allows us to update the version.
$filesystem->delete(storage_path('options.php'));
return json(trans('admin.update.complete'), 0);
} else {
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
return json($e->getMessage(), 1);
}
case 'progress':
return $package->progress();
default:
return json(trans('general.illegal-parameters'), 1);
}
}
protected function getUpdateInfo()
{
$response = Http::withOptions([
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get(config('app.update_source'));
if ($response->ok()) {
$info = $response->json();
if (Arr::get($info, 'spec') === self::SPEC) {
return ['ok' => true, 'info' => $info];
} else {
return ['ok' => false, 'error' => trans('admin.update.errors.spec')];
$acceptableSpec = 2;
if (app()->runningUnitTests() || ! $this->info) {
try {
$json = $this->guzzle->request(
'GET',
$this->updateSource,
['verify' => resource_path('misc/ca-bundle.crt')]
)->getBody();
$info = json_decode($json, true);
if (Arr::get($info, 'spec') == $acceptableSpec) {
$this->info = $info;
} else {
$this->error = trans('admin.update.errors.spec');
}
} catch (Exception $e) {
$this->error = $e->getMessage();
}
} else {
return ['ok' => false, 'error' => 'HTTP status code: '.$response->status()];
}
return $this->info;
}
protected function canUpdate($info = [])
protected function canUpdate()
{
$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]),
];
$this->getUpdateInfo();
$php = Arr::get($this->info, 'php');
if (Comparator::lessThan(PHP_VERSION, $php)) {
$this->error = trans('admin.update.errors.php', ['version' => $php]);
return false;
}
$can = Comparator::greaterThan(Arr::get($info, 'latest'), config('app.version'));
return ['can' => $can, 'reason' => ''];
return Comparator::greaterThan(Arr::get($this->info, 'latest'), $this->currentVersion);
}
}

View File

@ -2,145 +2,130 @@
namespace App\Http\Controllers;
use App\Events\UserProfileUpdated;
use App\Mail\EmailVerification;
use App\Models\Texture;
use App;
use URL;
use Mail;
use View;
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 App\Mail\EmailVerification;
use App\Events\UserProfileUpdated;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\URL;
use League\CommonMark\GithubFlavoredMarkdownConverter;
class UserController extends Controller
{
public function user()
public function __construct()
{
/** @var User */
$user = auth()->user();
$this->middleware(function ($request, $next) {
if (! Auth::user()->verified) {
$this->sendVerificationEmail();
}
return $user
->makeHidden(['password', 'ip', 'remember_token', 'verification_token']);
return $next($request);
})->only(['index', 'profile']);
}
public function index(Filter $filter)
public function user()
{
return json('', 0, auth()->user()->makeHidden(['password', 'ip', 'remember_token'])->toArray());
}
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'),
'statistics' => [
'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')),
'storage' => $this->calculatePercentageUsed($user->getStorageUsed(), option('score_per_storage')),
],
'announcement' => $converter->convertToHtml(option_localized('announcement')),
'grid' => $grid,
'extra' => ['unverified' => option('require_verification') && !$user->verified],
'announcement' => app('parsedown')->text(option_localized('announcement')),
'extra' => ['unverified' => option('require_verification') && ! $user->verified],
]);
}
public function scoreInfo()
{
/** @var User */
$user = Auth::user();
return response()->json([
return json('', 0, [
'user' => [
'score' => $user->score,
'lastSignAt' => $user->last_sign_at,
],
'rate' => [
'storage' => (int) option('score_per_storage'),
'players' => (int) option('score_per_player'),
'stats' => [
'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')),
'storage' => $this->calculatePercentageUsed($user->getStorageUsed(), option('score_per_storage')),
],
'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'),
'signAfterZero' => option('sign_after_zero'),
'signGapTime' => option('sign_gap_time'),
]);
}
public function sign(Dispatcher $dispatcher, Filter $filter)
/**
* 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;
$can = $filter->apply('can_sign', true);
if ($can instanceof Rejection) {
return json($can->getReason(), 2);
if ($rate != 0) {
$result['total'] = $used + floor($user->score / $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);
$dispatcher->dispatch('user.sign.before', [$acquiredScore]);
$user->score += $acquiredScore;
$user->last_sign_at = Carbon::now();
$user->save();
$dispatcher->dispatch('user.sign.after', [$acquiredScore]);
/**
* Handle user signing.
*
* @return \Illuminate\Http\JsonResponse
*/
public function sign()
{
$user = Auth::user();
if ($user->canSign()) {
$acquiredScore = $user->sign();
$gap = option('sign_gap_time');
return json(trans('user.sign-success', ['score' => $acquiredScore]), 0, [
'score' => $user->score,
'storage' => $this->calculatePercentageUsed($user->getStorageUsed(), option('score_per_storage')),
'remaining_time' => $gap > 1 ? round($gap) : $gap,
]);
} 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($user = null)
{
$hours = ($user ?? Auth::user())->getSignRemainingTime() / 3600;
return $hours > 1 ? round($hours) : $hours;
}
public function sendVerificationEmail()
{
if (!option('require_verification')) {
if (! option('require_verification')) {
return json(trans('user.verification.disabled'), 1);
}
@ -157,11 +142,12 @@ class UserController extends Controller
return json(trans('user.verification.verified'), 1);
}
$url = URL::signedRoute('auth.verify', ['user' => $user], null, false);
$url = URL::signedRoute('auth.verify', ['uid' => $user->uid]);
try {
Mail::to($user->email)->send(new EmailVerification(url($url)));
Mail::to($user->email)->send(new EmailVerification($url));
} catch (\Exception $e) {
// Write the exception to log
report($e);
return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2);
@ -172,100 +158,77 @@ 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'));
->with('extra', [
'unverified' => option('require_verification') && ! $user->verified,
'admin' => $user->isAdmin(),
]);
}
public function handleProfile(Request $request, Filter $filter, Dispatcher $dispatcher)
public function handleProfile(Request $request)
{
$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']);
if (option('single_player', false)) {
return json(trans('user.profile.nickname.single'), 1);
}
$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([
$this->validate($request, [
'current_password' => 'required|min:6|max:32',
'new_password' => 'required|min:8|max:32',
'new_password' => 'required|min:8|max:32',
]);
if (!$user->verifyPassword($request->input('current_password'))) {
if (! $user->verifyPassword($request->input('current_password'))) {
return json(trans('user.profile.password.wrong-password'), 1);
}
$user->changePassword($request->input('new_password'));
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
event(new UserProfileUpdated($action, $user));
if ($user->changePassword($request->input('new_password'))) {
event(new UserProfileUpdated($action, $user));
Auth::logout();
Auth::logout();
return json(trans('user.profile.password.success'), 0);
return json(trans('user.profile.password.success'), 0);
}
break; // @codeCoverageIgnore
case 'email':
$data = $request->validate([
'email' => 'required|email',
'password' => 'required|min:6|max:32',
$this->validate($request, [
'new_email' => 'required|email',
'password' => 'required|min:6|max:32',
]);
if (User::where('email', $data['email'])->count() > 0) {
if (User::where('email', $request->new_email)->count() > 0) {
return json(trans('user.profile.email.existed'), 1);
}
if (!$user->verifyPassword($data['password'])) {
if (! $user->verifyPassword($request->input('password'))) {
return json(trans('user.profile.email.wrong-password'), 1);
}
$user->email = $data['email'];
$user->email = $request->input('new_email');
$user->verified = false;
$user->save();
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
event(new UserProfileUpdated($action, $user));
Auth::logout();
@ -273,7 +236,7 @@ class UserController extends Controller
return json(trans('user.profile.email.success'), 0);
case 'delete':
$request->validate([
$this->validate($request, [
'password' => 'required|min:6|max:32',
]);
@ -281,80 +244,77 @@ class UserController extends Controller
return json(trans('user.profile.delete.admin'), 1);
}
if (!$user->verifyPassword($request->input('password'))) {
if (! $user->verifyPassword($request->input('password'))) {
return json(trans('user.profile.delete.wrong-password'), 1);
}
Auth::logout();
$dispatcher->dispatch('user.deleting', [$user]);
if ($user->delete()) {
session()->flush();
$user->delete();
$dispatcher->dispatch('user.deleted', [$user]);
session()->flush();
return json(trans('user.profile.delete.success'), 0);
}
return json(trans('user.profile.delete.success'), 0);
break; // @codeCoverageIgnore
default:
return json(trans('general.illegal-parameters'), 1);
break;
}
}
public function setAvatar(Request $request, Filter $filter, Dispatcher $dispatcher)
// @codeCoverageIgnore
/**
* Set user avatar.
*
* @param Request $request
*/
public function setAvatar(Request $request)
{
$request->validate(['tid' => 'required|integer']);
$this->validate($request, [
'tid' => 'required|integer',
]);
$tid = $request->input('tid');
/** @var User */
$user = auth()->user();
$can = $filter->apply('user_can_update_avatar', true, [$user, $tid]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$dispatcher->dispatch('user.avatar.updating', [$user, $tid]);
if ($tid == 0) {
$user->avatar = 0;
$user->save();
$dispatcher->dispatch('user.avatar.updated', [$user, $tid]);
return json(trans('user.profile.avatar.success'), 0);
}
$texture = Texture::find($tid);
if ($texture) {
if ($texture->type == 'cape') {
$result = Texture::find($tid);
if ($result) {
if ($result->type == 'cape') {
return json(trans('user.profile.avatar.wrong-type'), 1);
}
if (
!$texture->public
&& $user->uid !== $texture->uploader
&& !$user->isAdmin()
) {
return json(trans('skinlib.show.private'), 1);
}
$user->avatar = $tid;
$user->save();
$dispatcher->dispatch('user.avatar.updated', [$user, $tid]);
return json(trans('user.profile.avatar.success'), 0);
} else {
return json(trans('skinlib.non-existent'), 1);
}
}
public function toggleDarkMode()
public function readNotification($id)
{
/** @var User */
$user = auth()->user();
$user->is_dark_mode = !$user->is_dark_mode;
$user->save();
$notification = auth()
->user()
->unreadNotifications
->first(function ($notification) use ($id) {
return $notification->id === $id;
});
$notification->markAsRead();
return response()->noContent();
return [
'title' => $notification->data['title'],
'content' => app('parsedown')->text($notification->data['content']),
'time' => $notification->created_at->toDateTimeString(),
];
}
}

View File

@ -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);
}
}

View File

@ -11,61 +11,59 @@ 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\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
Middleware\ConvertEmptyStringsToNull::class,
Middleware\DetectLanguagePrefer::class,
\App\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\DetectLanguagePrefer::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,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\ForbiddenIE::class,
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
Middleware\EnforceEverGreen::class,
Middleware\RedirectToSetup::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'bindings',
],
'authorize' => [
'auth:web',
Middleware\RejectBannedUser::class,
Middleware\EnsureEmailFilled::class,
Middleware\FireUserAuthenticated::class,
\App\Http\Middleware\RejectBannedUser::class,
\App\Http\Middleware\EnsureEmailFilled::class,
\App\Http\Middleware\FireUserAuthenticated::class,
],
];
/**
* 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 = [
'auth' => \App\Http\Middleware\Authenticate::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::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,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
}

View File

@ -8,13 +8,13 @@ class Authenticate extends Middleware
{
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
if (! $request->expectsJson()) {
session([
'last_requested_path' => $request->fullUrl(),
'msg' => trans('auth.check.anonymous'),
]);
return route('auth.login');
return '/auth/login';
}
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Middleware;
class CheckAdministrator
{
public function handle($request, \Closure $next)
{
abort_unless(auth()->user()->isAdmin(), 403, trans('auth.check.admin'));
return $next($request);
}
}

View File

@ -2,14 +2,17 @@
namespace App\Http\Middleware;
use Illuminate\Filesystem\Filesystem;
use App\Http\Controllers\SetupController;
class CheckInstallation
{
public function handle($request, \Closure $next)
{
$hasLock = resolve(Filesystem::class)->exists(storage_path('install.lock'));
if ($hasLock) {
if (config('database.default') == 'dummy') {
return $next($request); // @codeCoverageIgnore
}
if (SetupController::checkTablesExist()) {
return response()->view('setup.locked');
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Middleware;
use Event;
use App\Models\Player;
use Illuminate\Support\Arr;
use App\Events\CheckPlayerExists;
class CheckPlayerExist
{
public function handle($request, \Closure $next)
{
$pid = Arr::get($request->route()->parameters, 'pid') ?? $request->input('pid');
if (! $request->isMethod('get') && ! is_null($pid)) {
if (is_null(Player::find($pid))) {
return json(trans('general.unexistent-player'), 1);
} else {
return $next($request);
}
}
if (stripos($request->getUri(), '.json') != false) {
preg_match('/\/([^\/]*)\.json/', $request->getUri(), $matches);
} else {
preg_match('/\/([^\/]*)\.png/', $request->getUri(), $matches);
}
$player_name = urldecode($matches[1]);
$responses = event(new CheckPlayerExists($player_name));
if (is_array($responses)) {
// @codeCoverageIgnoreStart
foreach ($responses as $r) {
if ($r) {
return $next($request);
}
}
// @codeCoverageIgnoreEnd
}
if (! Player::where('name', $player_name)->get()->isEmpty()) {
return $next($request);
}
if (option('return_204_when_notfound')) {
return response('', 204, [
'Cache-Control' => 'public, max-age='.option('cache_expire_time'),
]);
} else {
return abort(404, trans('general.unexistent-player'));
}
}
}

Some files were not shown because too many files have changed in this diff Show More